diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..d12b63f
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,38 @@
+# Operating System Files
+
+*.DS_Store
+Thumbs.db
+*.sw?
+.#*
+*#
+*~
+*.sublime-*
+
+# Build Artifacts
+
+.gradle/
+build/
+target/
+bin/
+out/
+dependency-reduced-pom.xml
+
+# Eclipse Project Files
+
+.classpath
+.project
+.metadata
+.loadpath
+bin/
+.settings/
+
+# IntelliJ IDEA Files
+
+*.iml
+*.ipr
+*.iws
+*.idea
+
+.vscode/
+README.html
+
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..3ca5ecb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,5 @@
+# Core Spring and Spring Boot Lab Projects
+
+Labs for the Core Spring and Spring Boot courses
+
+To import these labs into your IDE, import the parent pom `lab/pom.xml` as Maven projects or `lab/build.gradle` as Gradle projects.
diff --git a/lab/.mvn/wrapper/maven-wrapper.properties b/lab/.mvn/wrapper/maven-wrapper.properties
new file mode 100755
index 0000000..48a56c9
--- /dev/null
+++ b/lab/.mvn/wrapper/maven-wrapper.properties
@@ -0,0 +1,19 @@
+# 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.
+wrapperVersion=3.3.2
+distributionType=only-script
+distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.6/apache-maven-3.9.6-bin.zip
diff --git a/lab/00-rewards-common/build.gradle b/lab/00-rewards-common/build.gradle
new file mode 100644
index 0000000..38c7275
--- /dev/null
+++ b/lab/00-rewards-common/build.gradle
@@ -0,0 +1,6 @@
+apply plugin: 'java-library'
+
+dependencies {
+ api "org.hibernate.orm:hibernate-core"
+ api "com.fasterxml.jackson.core:jackson-annotations"
+}
diff --git a/lab/00-rewards-common/pom.xml b/lab/00-rewards-common/pom.xml
new file mode 100644
index 0000000..e93fdd9
--- /dev/null
+++ b/lab/00-rewards-common/pom.xml
@@ -0,0 +1,25 @@
+
+
+ 4.0.0
+ 00-rewards-common
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ com.fasterxml.jackson.core
+ jackson-annotations
+
+
+ org.hibernate.orm
+ hibernate-core
+
+
+
diff --git a/lab/00-rewards-common/src/main/java/common/datetime/DateInterval.java b/lab/00-rewards-common/src/main/java/common/datetime/DateInterval.java
new file mode 100644
index 0000000..bab4956
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/datetime/DateInterval.java
@@ -0,0 +1,21 @@
+package common.datetime;
+
+public class DateInterval {
+
+ private SimpleDate start;
+
+ private SimpleDate end;
+
+ public DateInterval(SimpleDate start, SimpleDate end) {
+ this.start = start;
+ this.end = end;
+ }
+
+ public SimpleDate getStart() {
+ return start;
+ }
+
+ public SimpleDate getEnd() {
+ return end;
+ }
+}
diff --git a/lab/00-rewards-common/src/main/java/common/datetime/SimpleDate.java b/lab/00-rewards-common/src/main/java/common/datetime/SimpleDate.java
new file mode 100644
index 0000000..b2a1926
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/datetime/SimpleDate.java
@@ -0,0 +1,112 @@
+package common.datetime;
+
+import java.io.Serializable;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+import java.text.SimpleDateFormat;
+
+/**
+ * A simple wrapper around a calendar for working with dates like 12/29/1977. Does not consider time.
+ */
+public class SimpleDate implements Serializable {
+
+ private static final long serialVersionUID = 2285962420279644602L;
+
+ private GregorianCalendar base;
+
+ /**
+ * Create a new simple date.
+ * @param month the month
+ * @param day the day
+ * @param year the year
+ */
+ public SimpleDate(int month, int day, int year) {
+ init(new GregorianCalendar(year, month - 1, day));
+ }
+
+ SimpleDate(long time) {
+ GregorianCalendar cal = new GregorianCalendar();
+ cal.setTimeInMillis(time);
+ init(cal);
+ }
+
+ private SimpleDate() {
+ init(new GregorianCalendar());
+ }
+
+ private void init(GregorianCalendar cal) {
+ this.base = trimToDays(cal);
+ }
+
+ private GregorianCalendar trimToDays(GregorianCalendar cal) {
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ return cal;
+ }
+
+ /**
+ * Returns this simple date as a java.util.Date
+ * @return this simple date as a Date
+ */
+ public Date asDate() {
+ return base.getTime();
+ }
+
+ /**
+ * Returns this date in milliseconds since 1970.
+ * @return
+ */
+ public long inMilliseconds() {
+ return asDate().getTime();
+ }
+
+ public int compareTo(Object date) {
+ SimpleDate other = (SimpleDate) date;
+ return asDate().compareTo(other.asDate());
+ }
+
+ public boolean equals(Object day) {
+ if (!(day instanceof SimpleDate other)) {
+ return false;
+ }
+ return (base.equals(other.base));
+ }
+
+ public int hashCode() {
+ return 29 * base.hashCode();
+ }
+
+ /**
+ * Returns todays date. A convenient static factory method.
+ */
+ public static SimpleDate today() {
+ return new SimpleDate();
+ }
+
+ /**
+ * Converts the specified date to a SimpleDate. Will trim hour, minute, second, and millisecond fields.
+ * @param date the java.util.Date
+ * @return the simple date
+ */
+ public static SimpleDate valueOf(Date date) {
+ return valueOf(date.getTime());
+ }
+
+ /**
+ * Converts the specified long time value to a SimpleDate. Will trim hour, minute, second, and millisecond fields.
+ * @param time time in millseconds since 1970
+ * @return the time as a SimpleDate
+ */
+ public static SimpleDate valueOf(long time) {
+ return new SimpleDate(time);
+ }
+
+ @Override
+ public String toString() {
+ return new SimpleDateFormat().format(base.getTime());
+ }
+
+}
\ No newline at end of file
diff --git a/lab/00-rewards-common/src/main/java/common/datetime/SimpleDateEditor.java b/lab/00-rewards-common/src/main/java/common/datetime/SimpleDateEditor.java
new file mode 100644
index 0000000..dc1b438
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/datetime/SimpleDateEditor.java
@@ -0,0 +1,34 @@
+package common.datetime;
+
+import java.beans.PropertyEditorSupport;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.util.Locale;
+
+/**
+ * A formatter for Simple date properties. Converts object values to well-formatted strings and strings back to
+ * values. Usable by a data binding framework for binding user input to the model.
+ */
+public class SimpleDateEditor extends PropertyEditorSupport {
+
+ private final DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.LONG, Locale.ENGLISH);
+
+ @Override
+ public String getAsText() {
+ SimpleDate date = (SimpleDate) getValue();
+ if (date == null) {
+ return "";
+ } else {
+ return dateFormat.format(date.asDate());
+ }
+ }
+
+ @Override
+ public void setAsText(String text) throws IllegalArgumentException {
+ try {
+ setValue(SimpleDate.valueOf(dateFormat.parse(text)));
+ } catch (ParseException e) {
+ throw new IllegalArgumentException("Unable to convert String '" + text + "' to a SimpleDate", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/00-rewards-common/src/main/java/common/datetime/package.html b/lab/00-rewards-common/src/main/java/common/datetime/package.html
new file mode 100644
index 0000000..d3412af
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/datetime/package.html
@@ -0,0 +1,7 @@
+
+
+
+Shared classes for representing date and time.
+
+
+
diff --git a/lab/00-rewards-common/src/main/java/common/money/MonetaryAmount.java b/lab/00-rewards-common/src/main/java/common/money/MonetaryAmount.java
new file mode 100644
index 0000000..fe7c660
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/money/MonetaryAmount.java
@@ -0,0 +1,167 @@
+package common.money;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import jakarta.persistence.Embeddable;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * A representation of money.
+ *
+ * A value object. Immutable.
+ */
+@Embeddable
+public class MonetaryAmount implements Serializable {
+
+ private static final long serialVersionUID = -3734467432803577280L;
+
+ private BigDecimal value;
+
+ /**
+ * Create a new monetary amount from the specified value.
+ * @param value the value of the amount; for example, in $USD "10.00" would be ten dollars, ".29" would be 29 cents
+ */
+ @JsonCreator
+ public MonetaryAmount(BigDecimal value) {
+ initValue(value);
+ }
+
+ /**
+ * Create a new monetary amount from the specified value.
+ * @param value the monetary amount as a double
+ */
+ public MonetaryAmount(double value) {
+ initValue(BigDecimal.valueOf(value));
+ }
+
+ @SuppressWarnings("unused")
+ private MonetaryAmount() {
+ }
+
+ private void initValue(BigDecimal value) {
+ this.value = value.setScale(2, RoundingMode.HALF_EVEN);
+ }
+
+ /**
+ * Convert the string representation of a monetary amount (e.g. $5 or 5) to a MonetaryAmount object.
+ * @param string the monetary amount string
+ * @return the monetary amount object
+ */
+ public static MonetaryAmount valueOf(String string) {
+ if (string == null || string.length() == 0) {
+ throw new IllegalArgumentException("The monetary amount value is required");
+ }
+ if (string.startsWith("$")) {
+ int index = string.indexOf('$');
+ string = string.substring(index + 1);
+ }
+ BigDecimal value = new BigDecimal(string);
+ return new MonetaryAmount(value);
+ }
+
+ /**
+ * Returns the zero (0.00) monetary amount.
+ */
+ public static MonetaryAmount zero() {
+ return new MonetaryAmount(0);
+ }
+
+ /**
+ * Add to this monetary amount, returning the sum as a new monetary amount.
+ * @param amount the amount to add
+ * @return the sum
+ */
+ public MonetaryAmount add(MonetaryAmount amount) {
+ return new MonetaryAmount(value.add(amount.value));
+ }
+
+ /**
+ * Subtract from this monetary amount, returning the difference as a new monetary amount.
+ * @param amount the amount to subtract
+ * @return the difference
+ */
+ public MonetaryAmount subtract(MonetaryAmount amount) {
+ return new MonetaryAmount(value.subtract(amount.value));
+ }
+
+ /**
+ * Multiply this monetary amount, returning the product as a new monetary amount.
+ * @param amount the amount to multiply
+ * @return the product
+ */
+ public MonetaryAmount multiplyBy(BigDecimal amount) {
+ return new MonetaryAmount(value.multiply(amount));
+ }
+
+ /**
+ * Divide this monetary amount, returning the quotient as a decimal.
+ * @param amount the amount to divide by
+ * @return the quotient
+ */
+ public BigDecimal divide(MonetaryAmount amount) {
+ return value.divide(amount.value);
+ }
+
+ /**
+ * Divide this monetary amount, returning the quotient as a new monetary amount.
+ * @param amount the amount to divide by
+ * @return the quotient
+ */
+ public MonetaryAmount divideBy(BigDecimal amount) {
+ return new MonetaryAmount(value.divide(amount));
+ }
+
+ /**
+ * Multiply this monetary amount by a percentage.
+ * @param percentage the percentage
+ * @return the percentage amount
+ */
+ public MonetaryAmount multiplyBy(Percentage percentage) {
+ return new MonetaryAmount(value.multiply(percentage.asBigDecimal()));
+ }
+
+ /**
+ * Returns true if this amount is greater than the amount.
+ * @param amount the monetary amount
+ * @return true or false
+ */
+ public boolean greaterThan(MonetaryAmount amount) {
+ return value.compareTo(amount.value) > 0;
+ }
+
+ /**
+ * Get this amount as a double. Useful for when a double type is needed by an external API or system.
+ * @return this amount as a double
+ */
+ public double asDouble() {
+ return value.doubleValue();
+ }
+
+ /**
+ * Get this amount as a big decimal. Useful for when a BigDecimal type is needed by an external API or system.
+ * @return this amount as a big decimal
+ */
+ @JsonValue
+ public BigDecimal asBigDecimal() {
+ return value;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof MonetaryAmount)) {
+ return false;
+ }
+ return value.equals(((MonetaryAmount) o).value);
+ }
+
+ public int hashCode() {
+ return value.hashCode();
+ }
+
+ public String toString() {
+ return "$" + value.toString();
+ }
+
+}
\ No newline at end of file
diff --git a/lab/00-rewards-common/src/main/java/common/money/MonetaryAmountEditor.java b/lab/00-rewards-common/src/main/java/common/money/MonetaryAmountEditor.java
new file mode 100644
index 0000000..c50d045
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/money/MonetaryAmountEditor.java
@@ -0,0 +1,33 @@
+package common.money;
+
+import java.beans.PropertyEditorSupport;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * A formatter for Monetary amount properties. Converts object values to well-formatted strings and strings back to
+ * values. Usable by a data binding framework for binding user input to the model.
+ */
+public class MonetaryAmountEditor extends PropertyEditorSupport {
+
+ @Override
+ public String getAsText() {
+ MonetaryAmount amount = (MonetaryAmount) getValue();
+ if (amount == null) {
+ return "";
+ } else {
+ return amount.toString();
+ }
+ }
+
+ @Override
+ public void setAsText(String text) throws IllegalArgumentException {
+ if (StringUtils.hasText(text)) {
+ setValue(MonetaryAmount.valueOf(text));
+ } else {
+ setValue(null);
+ }
+ }
+
+
+}
diff --git a/lab/00-rewards-common/src/main/java/common/money/Percentage.java b/lab/00-rewards-common/src/main/java/common/money/Percentage.java
new file mode 100644
index 0000000..ee305a0
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/money/Percentage.java
@@ -0,0 +1,135 @@
+package common.money;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonValue;
+
+import jakarta.persistence.Embeddable;
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+
+/**
+ * A percentage. Represented as a decimal value with scale 2 between 0.00 and 1.00.
+ *
+ * A value object. Immutable.
+ */
+@Embeddable
+public class Percentage implements Serializable {
+
+ private static final long serialVersionUID = 8077279865855620752L;
+
+ private BigDecimal value;
+
+ /**
+ * Create a new percentage from the specified value. Value must be between 0 and 1. For example, value .45
+ * represents 45%. If the value has more than two digits past the decimal point it will be rounded up. For example,
+ * value .24555 rounds up to .25.
+ * @param the percentage value
+ * @throws IllegalArgumentException if the value is not between 0 and 1
+ */
+ @JsonCreator
+ public Percentage(BigDecimal value) {
+ initValue(value);
+ }
+
+ /**
+ * Create a new percentage from the specified double value. Converts it to a BigDecimal with exact precision. Value
+ * must be between 0 and 1. For example, value .45 represents 45%. If the value has more than two digits past the
+ * decimal point it will be rounded up. For example, value .24555 rounds up to .25.
+ * @param the percentage value as a double
+ * @throws IllegalArgumentException if the value is not between 0 and 1
+ */
+ public Percentage(double value) {
+ initValue(BigDecimal.valueOf(value));
+ }
+
+ @SuppressWarnings("unused")
+ private Percentage() {
+ }
+
+ private void initValue(BigDecimal value) {
+ value = value.setScale(2, RoundingMode.HALF_UP);
+ if (value.compareTo(BigDecimal.ZERO) == -1 || value.compareTo(BigDecimal.ONE) == 1) {
+ throw new IllegalArgumentException("Percentage value must be between 0 and 1; your value was " + value);
+ }
+ this.value = value;
+ }
+
+ /**
+ * Convert the string representation of a percentage (e.g. 5% or 5) to a Percentage object.
+ * @param string the percentage string
+ * @return the percentage object
+ */
+ public static Percentage valueOf(String string) {
+ if (string == null || string.length() == 0) {
+ throw new IllegalArgumentException("The percentage value is required");
+ }
+ boolean percent = string.endsWith("%");
+ if (percent) {
+ int index = string.lastIndexOf('%');
+ string = string.substring(0, index);
+ }
+ BigDecimal value = new BigDecimal(string);
+ if (percent) {
+ value = value.divide(new BigDecimal(100));
+ }
+ return new Percentage(value);
+ }
+
+ /**
+ * Returns zero percent.
+ */
+ public static Percentage zero() {
+ return new Percentage(0);
+ }
+
+ /**
+ * Returns one hundred percent.
+ */
+ public static Percentage oneHundred() {
+ return new Percentage(1);
+ }
+
+ /**
+ * Add to this percentage.
+ * @param percentage the percentage to add
+ * @return the sum
+ * @throws IllegalArgumentException if the new percentage exceeds 1
+ */
+ public Percentage add(Percentage percentage) throws IllegalArgumentException {
+ return new Percentage(value.add(percentage.value));
+ }
+
+ /**
+ * Return this percentage as a double. Useful for when a double type is needed by an external API or system.
+ * @return this percentage as a double
+ */
+ public double asDouble() {
+ return value.doubleValue();
+ }
+
+ /**
+ * Return this percentage as a big decimal. Useful for when a big decimal type is needed by an external API or
+ * system.
+ * @return this percentage as a big decimal
+ */
+ @JsonValue
+ public BigDecimal asBigDecimal() {
+ return value;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Percentage)) {
+ return false;
+ }
+ return value.equals(((Percentage) o).value);
+ }
+
+ public int hashCode() {
+ return value.hashCode();
+ }
+
+ public String toString() {
+ return value.multiply(new BigDecimal("100")).setScale(0) + "%";
+ }
+}
\ No newline at end of file
diff --git a/lab/00-rewards-common/src/main/java/common/money/PercentageEditor.java b/lab/00-rewards-common/src/main/java/common/money/PercentageEditor.java
new file mode 100644
index 0000000..6fb1951
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/money/PercentageEditor.java
@@ -0,0 +1,32 @@
+package common.money;
+
+import java.beans.PropertyEditorSupport;
+
+import org.springframework.util.StringUtils;
+
+/**
+ * A formatter for Percentage properties. Converts object values to well-formatted strings and strings back to values.
+ * Usable by a data binding framework for binding user input to the model.
+ */
+public class PercentageEditor extends PropertyEditorSupport {
+
+ @Override
+ public String getAsText() {
+ Percentage percentage = (Percentage) getValue();
+ if (percentage == null) {
+ return "";
+ } else {
+ return percentage.toString();
+ }
+ }
+
+ @Override
+ public void setAsText(String text) throws IllegalArgumentException {
+ if (StringUtils.hasText(text)) {
+ setValue(Percentage.valueOf(text));
+ } else {
+ setValue(null);
+ }
+ }
+
+}
diff --git a/lab/00-rewards-common/src/main/java/common/money/package.html b/lab/00-rewards-common/src/main/java/common/money/package.html
new file mode 100644
index 0000000..0f26989
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/money/package.html
@@ -0,0 +1,7 @@
+
+
+
+Shared classes for representing money.
+
+
+
diff --git a/lab/00-rewards-common/src/main/java/common/repository/Entity.java b/lab/00-rewards-common/src/main/java/common/repository/Entity.java
new file mode 100644
index 0000000..c17b2ac
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/repository/Entity.java
@@ -0,0 +1,34 @@
+package common.repository;
+
+/**
+ * A base class for all entities that use an internal long identifier for
+ * tracking entity identity.
+ */
+public class Entity {
+
+ private Long entityId;
+
+ /**
+ * Returns the entity identifier used to internally distinguish this entity
+ * among other entities of the same type in the system. Should typically
+ * only be called by privileged data access infrastructure code such as an
+ * Object Relational Mapper (ORM) and not by application code.
+ *
+ * @return the internal entity identifier
+ */
+ public Long getEntityId() {
+ return entityId;
+ }
+
+ /**
+ * Sets the internal entity identifier - should only be called by privileged
+ * data access code: repositories that work with an Object Relational Mapper
+ * (ORM). Should never be set by application code explicitly.
+ *
+ * @param entityId
+ * the internal entity identifier
+ */
+ public void setEntityId(Long entityId) {
+ this.entityId = entityId;
+ }
+}
diff --git a/lab/00-rewards-common/src/main/java/common/repository/package.html b/lab/00-rewards-common/src/main/java/common/repository/package.html
new file mode 100644
index 0000000..8134693
--- /dev/null
+++ b/lab/00-rewards-common/src/main/java/common/repository/package.html
@@ -0,0 +1,7 @@
+
+
+
+Shared classes for persisting business entities such as Account.
+
+
+
diff --git a/lab/00-rewards-common/src/main/resources/banner.txt b/lab/00-rewards-common/src/main/resources/banner.txt
new file mode 100644
index 0000000..e5c92ab
--- /dev/null
+++ b/lab/00-rewards-common/src/main/resources/banner.txt
@@ -0,0 +1,8 @@
+${Ansi.GREEN} :: ____
+ /\ ____ ____ _ \ \ \
+|/\| / ___|___ _ __ ___ / ___| _ __ _ __(_)_ __ __ _ \ \ \
+ | | / _ \| '__/ _ \ \___ \| '_ \| '__| | '_ \ / _` | \ \ \
+ | |__| (_) | | | __/ ___) | |_) | | | | | | | (_| | / / /
+ \____\___/|_| \___| |____/| .__/|_| |_|_| |_|\__, | / / /
+ ================================|_|==================|___/=/_/_/=
+ :: Core Spring Training :: :: v3.3.1 ::${Ansi.DEFAULT}
diff --git a/lab/00-rewards-common/src/main/resources/logback.xml b/lab/00-rewards-common/src/main/resources/logback.xml
new file mode 100644
index 0000000..cc6eac1
--- /dev/null
+++ b/lab/00-rewards-common/src/main/resources/logback.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+ %-5p: %logger{40} - %msg%n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/lab/00-rewards-common/src/main/resources/rewards/testdb/data.sql b/lab/00-rewards-common/src/main/resources/rewards/testdb/data.sql
new file mode 100644
index 0000000..28a87cc
--- /dev/null
+++ b/lab/00-rewards-common/src/main/resources/rewards/testdb/data.sql
@@ -0,0 +1,78 @@
+
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456789', 'Keith and Keri Donald');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456001', 'Dollie R. Adams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456002', 'Cornelia J. Andresen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456003', 'Coral Villareal Betancourt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456004', 'Chad I. Cobbs');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456005', 'Michael C. Feller');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456006', 'Michael J. Grover');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456007', 'John C. Howard');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456008', 'Ida Ketterer');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456009', 'Laina Ochoa Lucero');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456010', 'Wesley M. Mayo');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456011', 'Leslie F. Mcclary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456012', 'John D. Mudra');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456013', 'Pietronella J. Nielsen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456014', 'John S. Oleary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456015', 'Glenda D. Smith');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456016', 'Willemina O. Thygesen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456017', 'Antje Vogt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456018', 'Julia Weber');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456019', 'Mark T. Williams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456020', 'Christine J. Wilson');
+
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (0, '1234123412341234');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (1, '1234123412340001');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (2, '1234123412340002');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (3, '1234123412340003');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (4, '1234123412340004');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (5, '1234123412340005');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (6, '1234123412340006');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (7, '1234123412340007');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (8, '1234123412340008');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (9, '1234123412340009');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (10, '1234123412340010');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (11, '1234123412340011');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (12, '1234123412340012');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (13, '1234123412340013');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (14, '1234123412340014');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (15, '1234123412340015');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (16, '1234123412340016');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (17, '1234123412340017');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (18, '1234123412340018');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (19, '1234123412340019');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (20, '1234123412340020');
+
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (0, 'Annabelle', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (0, 'Corgan', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Antolin', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Argus', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Gian', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Argeo', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Kai', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Kasper', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Ernst', .34, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (12, 'Brian', .75, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (12, 'Shelby', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Charles', .50, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Thomas', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Neil', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (17, 'Daniel', 1.0, 0.00);
+
+insert into T_RESTAURANT (MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY)
+ values ('1234567890', 'AppleBees', .08, 'A');
diff --git a/lab/00-rewards-common/src/main/resources/rewards/testdb/mysql_data.sql b/lab/00-rewards-common/src/main/resources/rewards/testdb/mysql_data.sql
new file mode 100644
index 0000000..a96f90a
--- /dev/null
+++ b/lab/00-rewards-common/src/main/resources/rewards/testdb/mysql_data.sql
@@ -0,0 +1,77 @@
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456789', 'Keith and Keri Donald');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456001', 'Dollie R. Adams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456002', 'Cornelia J. Andresen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456003', 'Coral Villareal Betancourt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456004', 'Chad I. Cobbs');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456005', 'Michael C. Feller');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456006', 'Michael J. Grover');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456007', 'John C. Howard');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456008', 'Ida Ketterer');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456009', 'Laina Ochoa Lucero');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456010', 'Wesley M. Mayo');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456011', 'Leslie F. Mcclary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456012', 'John D. Mudra');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456013', 'Pietronella J. Nielsen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456014', 'John S. Oleary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456015', 'Glenda D. Smith');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456016', 'Willemina O. Thygesen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456017', 'Antje Vogt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456018', 'Julia Weber');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456019', 'Mark T. Williams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456020', 'Christine J. Wilson');
+
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (1, '1234123412341234');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (2, '1234123412340001');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (3, '1234123412340002');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (4, '1234123412340003');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (5, '1234123412340004');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (6, '1234123412340005');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (7, '1234123412340006');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (8, '1234123412340007');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (9, '1234123412340008');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (10, '1234123412340009');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (11, '1234123412340010');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (12, '1234123412340011');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (13, '1234123412340012');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (14, '1234123412340013');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (15, '1234123412340014');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (16, '1234123412340015');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (17, '1234123412340016');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (18, '1234123412340017');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (19, '1234123412340018');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (20, '1234123412340019');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (21, '1234123412340020');
+
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (1, 'Annabelle', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (1, 'Corgan', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (4, 'Antolin', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (4, 'Argus', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (4, 'Gian', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (4, 'Argeo', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (9, 'Kai', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (9, 'Kasper', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (9, 'Ernst', .34, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (13, 'Brian', .75, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (13, 'Shelby', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (16, 'Charles', .50, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (16, 'Thomas', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (16, 'Neil', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (18, 'Daniel', 1.0, 0.00);
+
+insert into T_RESTAURANT (MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY)
+ values ('1234567890', 'AppleBees', .08, 'A');
diff --git a/lab/00-rewards-common/src/main/resources/rewards/testdb/mysql_schema.sql b/lab/00-rewards-common/src/main/resources/rewards/testdb/mysql_schema.sql
new file mode 100644
index 0000000..54e0c1f
--- /dev/null
+++ b/lab/00-rewards-common/src/main/resources/rewards/testdb/mysql_schema.sql
@@ -0,0 +1,16 @@
+drop table if exists T_ACCOUNT_BENEFICIARY;
+drop table if exists T_ACCOUNT_CREDIT_CARD;
+drop table if exists T_ACCOUNT;
+drop table if exists T_RESTAURANT;
+drop table if exists T_REWARD;
+drop table if exists DUAL_REWARD_CONFIRMATION_NUMBER;
+
+create table T_ACCOUNT (ID INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(ID), NUMBER varchar(9), NAME varchar(50));
+create table T_ACCOUNT_CREDIT_CARD (ID INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(ID), ACCOUNT_ID integer, NUMBER varchar(16), unique(ACCOUNT_ID, NUMBER));
+create table T_ACCOUNT_BENEFICIARY (ID INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(ID), ACCOUNT_ID integer, NAME varchar(50), ALLOCATION_PERCENTAGE decimal(3,2), SAVINGS decimal(8,2));
+create table T_RESTAURANT (ID INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(ID), MERCHANT_NUMBER varchar(10), NAME varchar(80), BENEFIT_PERCENTAGE decimal(3,2), BENEFIT_AVAILABILITY_POLICY varchar(1));
+create table T_REWARD (ID INT NOT NULL AUTO_INCREMENT, PRIMARY KEY(ID), CONFIRMATION_NUMBER varchar(25), REWARD_AMOUNT decimal(8,2), REWARD_DATE date, ACCOUNT_NUMBER varchar(9), DINING_AMOUNT decimal(8,2), DINING_MERCHANT_NUMBER varchar(10), DINING_DATE date);
+
+create table DUAL_REWARD_CONFIRMATION_NUMBER (ZERO int);
+insert into DUAL_REWARD_CONFIRMATION_NUMBER values (0);
+
diff --git a/lab/00-rewards-common/src/main/resources/rewards/testdb/schema.sql b/lab/00-rewards-common/src/main/resources/rewards/testdb/schema.sql
new file mode 100644
index 0000000..b0324fa
--- /dev/null
+++ b/lab/00-rewards-common/src/main/resources/rewards/testdb/schema.sql
@@ -0,0 +1,20 @@
+drop table T_ACCOUNT_BENEFICIARY if exists;
+drop table T_ACCOUNT_CREDIT_CARD if exists;
+drop table T_ACCOUNT if exists;
+drop table T_RESTAURANT if exists;
+drop table T_REWARD if exists;
+drop sequence S_REWARD_CONFIRMATION_NUMBER if exists;
+drop table DUAL_REWARD_CONFIRMATION_NUMBER if exists;
+
+create table T_ACCOUNT (ID integer identity primary key, NUMBER varchar(9), NAME varchar(50) not null, unique(NUMBER));
+create table T_ACCOUNT_CREDIT_CARD (ID integer identity primary key, ACCOUNT_ID integer, NUMBER varchar(16), unique(ACCOUNT_ID, NUMBER));
+create table T_ACCOUNT_BENEFICIARY (ID integer identity primary key, ACCOUNT_ID integer, NAME varchar(50), ALLOCATION_PERCENTAGE decimal(3,2) not null, SAVINGS decimal(8,2) not null, unique(ACCOUNT_ID, NAME));
+create table T_RESTAURANT (ID integer identity primary key, MERCHANT_NUMBER varchar(10) not null, NAME varchar(80) not null, BENEFIT_PERCENTAGE decimal(3,2) not null, BENEFIT_AVAILABILITY_POLICY varchar(1) not null, unique(MERCHANT_NUMBER));
+create table T_REWARD (ID integer identity primary key, CONFIRMATION_NUMBER varchar(25) not null, REWARD_AMOUNT decimal(8,2) not null, REWARD_DATE date not null, ACCOUNT_NUMBER varchar(9) not null, DINING_AMOUNT decimal not null, DINING_MERCHANT_NUMBER varchar(10) not null, DINING_DATE date not null, unique(CONFIRMATION_NUMBER));
+
+create sequence S_REWARD_CONFIRMATION_NUMBER start with 1;
+create table DUAL_REWARD_CONFIRMATION_NUMBER (ZERO integer);
+insert into DUAL_REWARD_CONFIRMATION_NUMBER values (0);
+
+alter table T_ACCOUNT_CREDIT_CARD add constraint FK_ACCOUNT_CREDIT_CARD foreign key (ACCOUNT_ID) references T_ACCOUNT(ID) on delete cascade;
+alter table T_ACCOUNT_BENEFICIARY add constraint FK_ACCOUNT_BENEFICIARY foreign key (ACCOUNT_ID) references T_ACCOUNT(ID) on delete cascade;
\ No newline at end of file
diff --git a/lab/00-rewards-common/src/test/java/common/datetime/SimpleDateEditorTests.java b/lab/00-rewards-common/src/test/java/common/datetime/SimpleDateEditorTests.java
new file mode 100644
index 0000000..28ea4f7
--- /dev/null
+++ b/lab/00-rewards-common/src/test/java/common/datetime/SimpleDateEditorTests.java
@@ -0,0 +1,32 @@
+package common.datetime;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+public class SimpleDateEditorTests {
+
+ private final SimpleDateEditor editor = new SimpleDateEditor();
+
+ @Test
+ public void testGetAsText() {
+ SimpleDate date = new SimpleDate(12, 29, 1977);
+ editor.setValue(date);
+ assertEquals("December 29, 1977", editor.getAsText());
+ }
+
+ @Test
+ public void testSetAsText() {
+ editor.setAsText("December 29, 1977");
+ SimpleDate date = (SimpleDate) editor.getValue();
+ assertEquals(new SimpleDate(12, 29, 1977), date);
+ }
+
+ @Test
+ public void testSetAsTextBogus() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ editor.setAsText("December 29th, 1977");
+ });
+ }
+}
diff --git a/lab/00-rewards-common/src/test/java/common/datetime/SimpleDateTests.java b/lab/00-rewards-common/src/test/java/common/datetime/SimpleDateTests.java
new file mode 100644
index 0000000..c006ba1
--- /dev/null
+++ b/lab/00-rewards-common/src/test/java/common/datetime/SimpleDateTests.java
@@ -0,0 +1,42 @@
+package common.datetime;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Calendar;
+import java.util.Date;
+import java.util.GregorianCalendar;
+
+/**
+ * Unit tests for the "Simple Date" wrapper around a Calendar that tracks
+ * month/date/year only, with no provision for tracking time.
+ */
+public class SimpleDateTests {
+
+ @Test
+ public void testToday() {
+ SimpleDate today = SimpleDate.today();
+ Calendar cal = new GregorianCalendar();
+ cal.set(Calendar.HOUR_OF_DAY, 0);
+ cal.set(Calendar.MINUTE, 0);
+ cal.set(Calendar.SECOND, 0);
+ cal.set(Calendar.MILLISECOND, 0);
+ assertEquals(today.asDate(), cal.getTime());
+ }
+
+ @Test
+ public void testValueOfDate() {
+ SimpleDate today = SimpleDate.today();
+ Date date = today.asDate();
+ SimpleDate today2 = SimpleDate.valueOf(date);
+ assertEquals(today, today2);
+ }
+
+ @Test
+ public void testValueOfTime() {
+ SimpleDate today = SimpleDate.today();
+ long time = today.inMilliseconds();
+ SimpleDate today2 = SimpleDate.valueOf(time);
+ assertEquals(today, today2);
+ }
+}
diff --git a/lab/00-rewards-common/src/test/java/common/money/MonetaryAmountTests.java b/lab/00-rewards-common/src/test/java/common/money/MonetaryAmountTests.java
new file mode 100644
index 0000000..a6a8322
--- /dev/null
+++ b/lab/00-rewards-common/src/test/java/common/money/MonetaryAmountTests.java
@@ -0,0 +1,61 @@
+package common.money;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.math.BigDecimal;
+
+/**
+ * Unit tests that make sure the MonetaryAmount class works in isolation.
+ */
+public class MonetaryAmountTests {
+ @Test
+ public void testMonetaryAmountValueOfString() {
+ MonetaryAmount amount = MonetaryAmount.valueOf("$100");
+ assertEquals("$100.00", amount.toString());
+ }
+
+ @Test
+ public void testMonetaryCreation() {
+ MonetaryAmount amt = MonetaryAmount.valueOf("100.00");
+ assertEquals("$100.00", amt.toString());
+ }
+
+ @Test
+ public void testMonetaryAdd() {
+ MonetaryAmount amt1 = MonetaryAmount.valueOf("100.00");
+ MonetaryAmount amt2 = MonetaryAmount.valueOf("100.00");
+ assertEquals(MonetaryAmount.valueOf("200.00"), amt1.add(amt2));
+ assertEquals("$200.00", amt1.add(amt2).toString());
+ }
+
+ @Test
+ public void testMultiplyByPercentage() {
+ MonetaryAmount amt = MonetaryAmount.valueOf("100.005");
+ assertEquals(MonetaryAmount.valueOf("8.00"), amt.multiplyBy(Percentage.valueOf("8%")));
+ }
+
+ @Test
+ public void testMultiplyByDecimal() {
+ MonetaryAmount amt = MonetaryAmount.valueOf("100.005");
+ assertEquals(MonetaryAmount.valueOf("8.00"), amt.multiplyBy(new BigDecimal("0.08")));
+ }
+
+ @Test
+ public void testDivideByMonetaryAmount() {
+ MonetaryAmount amt = MonetaryAmount.valueOf("100.005");
+ assertEquals(new BigDecimal("12.5"), amt.divide(MonetaryAmount.valueOf("8.00")));
+ }
+
+ @Test
+ public void testDivideByDecimal() {
+ MonetaryAmount amt = MonetaryAmount.valueOf("100.005");
+ assertEquals(MonetaryAmount.valueOf("8.00"), amt.divideBy(new BigDecimal("12.5")));
+ }
+
+ @Test
+ public void testDoubleEquality() {
+ MonetaryAmount amt = MonetaryAmount.valueOf(".1");
+ assertEquals(new BigDecimal(".10"), amt.asBigDecimal());
+ }
+}
diff --git a/lab/00-rewards-common/src/test/java/common/money/PercentageTests.java b/lab/00-rewards-common/src/test/java/common/money/PercentageTests.java
new file mode 100644
index 0000000..6564896
--- /dev/null
+++ b/lab/00-rewards-common/src/test/java/common/money/PercentageTests.java
@@ -0,0 +1,41 @@
+package common.money;
+
+import org.junit.jupiter.api.Test;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests that make sure the Percentage class works in isolation.
+ */
+public class PercentageTests {
+
+
+ @Test
+ public void testPercentageValueOfString() {
+ Percentage percentage = Percentage.valueOf("100%");
+ assertEquals("100%", percentage.toString());
+ }
+
+ @Test
+ public void testPercentage() {
+ assertEquals(Percentage.valueOf("0.01"), Percentage.valueOf("1%"));
+ }
+
+ @Test
+ public void testPercentageEquality() {
+ Percentage percentage1 = Percentage.valueOf("25%");
+ Percentage percentage2 = Percentage.valueOf("25%");
+ assertEquals(percentage1, percentage2);
+ }
+
+ @Test
+ public void testNewPercentage() {
+ Percentage p = new Percentage(.25);
+ assertEquals("25%", p.toString());
+ }
+
+ @Test
+ public void testNewPercentageWithRounding() {
+ Percentage p = new Percentage(.255555);
+ assertEquals("26%", p.toString());
+ }
+}
diff --git a/lab/01-rewards-db/build.gradle b/lab/01-rewards-db/build.gradle
new file mode 100644
index 0000000..b909d3e
--- /dev/null
+++ b/lab/01-rewards-db/build.gradle
@@ -0,0 +1,7 @@
+apply plugin: 'java-library'
+
+dependencies {
+ api project(':00-rewards-common')
+ api "org.springframework:spring-orm"
+ api "org.hibernate.orm:hibernate-core"
+}
diff --git a/lab/01-rewards-db/pom.xml b/lab/01-rewards-db/pom.xml
new file mode 100644
index 0000000..726ba8c
--- /dev/null
+++ b/lab/01-rewards-db/pom.xml
@@ -0,0 +1,29 @@
+
+
+ 4.0.0
+ 01-rewards-db
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+ org.springframework
+ spring-orm
+
+
+ org.hibernate.orm
+ hibernate-core
+
+
+
diff --git a/lab/01-rewards-db/src/main/java/accounts/AccountManager.java b/lab/01-rewards-db/src/main/java/accounts/AccountManager.java
new file mode 100644
index 0000000..b3cdcf8
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/accounts/AccountManager.java
@@ -0,0 +1,94 @@
+package accounts;
+
+import java.util.List;
+import java.util.Map;
+
+import rewards.internal.account.Account;
+
+import common.money.Percentage;
+
+/**
+ * Manages access to account information. Used as the service layer component in
+ * the mvc and security projects.
+ */
+public interface AccountManager {
+
+ /**
+ * Indicates implementation being used. Actual implementation is hidden
+ * behind a proxy, so this makes it easy to determine.
+ *
+ * @return Implementation information.
+ */
+ String getInfo();
+
+ /**
+ * Get all accounts in the system
+ *
+ * @return all accounts
+ */
+ List getAllAccounts();
+
+ /**
+ * Find an account by its number.
+ *
+ * @param id
+ * the account id
+ * @return the account
+ */
+ Account getAccount(Long id);
+
+ /**
+ * Takes a transient account and persists it.
+ *
+ * @param account
+ * The account to save
+ * @return The persistent account - this may or may not be the same object
+ * as the method argument.
+ */
+ Account save(Account account);
+
+ /**
+ * Takes a changed account and persists any changes made to it.
+ *
+ * @param account
+ * The account with changes
+ */
+ void update(Account account);
+
+ /**
+ * Updates the allocation percentages for the entire collection of
+ * beneficiaries in an account
+ *
+ * @param accountId
+ * the account id
+ * @param allocationPercentages
+ * A map of beneficiary names and allocation percentages, keyed
+ * by beneficiary name
+ */
+ void updateBeneficiaryAllocationPercentages(Long accountId,
+ Map allocationPercentages);
+
+ /**
+ * Adds a beneficiary to an account. The new beneficiary will have a 0
+ * allocation percentage.
+ *
+ * @param accountId
+ * the account id
+ * @param beneficiaryName
+ * the name of the beneficiary to remove
+ */
+ void addBeneficiary(Long accountId, String beneficiaryName);
+
+ /**
+ * Removes a beneficiary from an account.
+ *
+ * @param accountId
+ * the account id
+ * @param beneficiaryName
+ * the name of the beneficiary to remove
+ * @param allocationPercentages
+ * new allocation percentages, keyed by beneficiary name
+ */
+ void removeBeneficiary(Long accountId, String beneficiaryName,
+ Map allocationPercentages);
+}
diff --git a/lab/01-rewards-db/src/main/java/accounts/internal/AbstractAccountManager.java b/lab/01-rewards-db/src/main/java/accounts/internal/AbstractAccountManager.java
new file mode 100644
index 0000000..abc1496
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/accounts/internal/AbstractAccountManager.java
@@ -0,0 +1,23 @@
+package accounts.internal;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import accounts.AccountManager;
+
+public abstract class AbstractAccountManager implements AccountManager {
+
+ protected final Logger logger;
+
+ public AbstractAccountManager() {
+ logger = LoggerFactory.getLogger(getClass());
+ logger.info("Created " + getInfo() + " account-manager");
+ }
+
+ @Override
+ public String getInfo() {
+ String myClassName = getClass().getSimpleName();
+ int ix = myClassName.indexOf("AccountManager");
+ return ix == -1 ? "UNKNOWN" : myClassName.substring(0, ix).toUpperCase();
+ }
+}
diff --git a/lab/01-rewards-db/src/main/java/accounts/internal/JpaAccountManager.java b/lab/01-rewards-db/src/main/java/accounts/internal/JpaAccountManager.java
new file mode 100644
index 0000000..4c34ace
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/accounts/internal/JpaAccountManager.java
@@ -0,0 +1,109 @@
+package accounts.internal;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import common.money.Percentage;
+import rewards.internal.account.Account;
+
+/**
+ * An account manager that uses JPA to find accounts.
+ */
+@Repository
+public class JpaAccountManager extends AbstractAccountManager {
+
+ private EntityManager entityManager;
+
+ /**
+ * Creates a new JPA account manager.
+ *
+ * Its entityManager will be set automatically by
+ * {@link #setEntityManager(EntityManager)}.
+ */
+ public JpaAccountManager() {
+ }
+
+ @PersistenceContext
+ public void setEntityManager(EntityManager entityManager) {
+ this.entityManager = entityManager;
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ @SuppressWarnings("unchecked")
+ public List getAllAccounts() {
+ List l = entityManager.createQuery("select a from Account a LEFT JOIN FETCH a.beneficiaries")
+ .getResultList();
+
+ // Use of "JOIN FETCH" produces duplicate accounts, and DISTINCT does
+ // not address this. So we have to filter it manually.
+ List result = new ArrayList<>();
+
+ for (Account a : l) {
+ if (!result.contains(a))
+ result.add(a);
+ }
+
+ return result;
+ }
+
+ @Override
+ @Transactional(readOnly = true)
+ public Account getAccount(Long id) {
+ Account account = entityManager.find(Account.class, id);
+
+ if (account != null) {
+ // Force beneficiaries to load too - avoid Hibernate lazy loading error
+ account.getBeneficiaries().size();
+ }
+
+ return account;
+ }
+
+ @Override
+ @Transactional
+ public Account save(Account account) {
+ entityManager.persist(account);
+ return account;
+ }
+
+ @Override
+ @Transactional
+ public void update(Account account) {
+ entityManager.merge(account);
+ }
+
+ @Override
+ @Transactional
+ public void updateBeneficiaryAllocationPercentages(Long accountId, Map allocationPercentages) {
+ Account account = getAccount(accountId);
+ for (Entry entry : allocationPercentages.entrySet()) {
+ account.getBeneficiary(entry.getKey()).setAllocationPercentage(entry.getValue());
+ }
+ }
+
+ @Override
+ @Transactional
+ public void addBeneficiary(Long accountId, String beneficiaryName) {
+ getAccount(accountId).addBeneficiary(beneficiaryName, Percentage.zero());
+ }
+
+ @Override
+ @Transactional
+ public void removeBeneficiary(Long accountId, String beneficiaryName,
+ Map allocationPercentages) {
+ getAccount(accountId).removeBeneficiary(beneficiaryName);
+
+ if (allocationPercentages != null)
+ updateBeneficiaryAllocationPercentages(accountId, allocationPercentages);
+ }
+
+}
diff --git a/lab/01-rewards-db/src/main/java/accounts/internal/StubAccountManager.java b/lab/01-rewards-db/src/main/java/accounts/internal/StubAccountManager.java
new file mode 100644
index 0000000..d5be612
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/accounts/internal/StubAccountManager.java
@@ -0,0 +1,105 @@
+package accounts.internal;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.concurrent.atomic.AtomicLong;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.Beneficiary;
+
+import common.money.Percentage;
+
+/**
+ * IMPORTANT: Per best practices, this class shouldn't be in 'src/main/java'
+ * but rather in 'src/test/java'. However, it is used by numerous Test classes
+ * inside multiple projects. Maven does not provide an easy way to access a
+ * class that is inside another project's 'src/test/java' folder.
+ *
+ * Rather than using some complex Maven configuration, we decided it is
+ * acceptable to place this test class inside 'src/main/java'.
+ */
+public class StubAccountManager extends AbstractAccountManager {
+
+ public static final int NUM_ACCOUNTS_IN_STUB = 1;
+
+ public static final long TEST_ACCOUNT_ID = 0L;
+ public static final String TEST_ACCOUNT_NUMBER = "123456789";
+ public static final String TEST_ACCOUNT_NAME = "Keith and Keri Donald";
+
+ public static final long TEST_BEN0_ID = 0L;
+ public static final String TEST_BEN0_NAME = "Annabelle";
+ public static final long TEST_BEN1_ID = 1L;
+ public static final String TEST_BEN1_NAME = "Corgan";
+ public static final String BENEFICIARY_SHARE = "50%";
+
+ private final Map accountsById = new HashMap<>();
+
+ private final AtomicLong nextEntityId = new AtomicLong(3);
+
+ public StubAccountManager() {
+ // One test account
+ Account account = new Account(TEST_ACCOUNT_NUMBER, TEST_ACCOUNT_NAME);
+ account.setEntityId(TEST_ACCOUNT_ID);
+
+ // Two test beneficiaries
+ account.addBeneficiary(TEST_BEN0_NAME, Percentage.valueOf(BENEFICIARY_SHARE));
+ account.addBeneficiary(TEST_BEN1_NAME, Percentage.valueOf(BENEFICIARY_SHARE));
+
+ // Retrieve each Beneficiary and set its entityId
+ account.getBeneficiary(TEST_BEN0_NAME).setEntityId(TEST_BEN0_ID);
+ account.getBeneficiary(TEST_BEN1_NAME).setEntityId(TEST_BEN1_ID);
+
+ // Save the account
+ accountsById.put(0L, account);
+ }
+
+ @Override
+ public List getAllAccounts() {
+ return new ArrayList<>(accountsById.values());
+ }
+
+ @Override
+ public Account getAccount(Long id) {
+ return accountsById.get(id);
+ }
+
+ @Override
+ public Account save(Account newAccount) {
+ for (Beneficiary beneficiary : newAccount.getBeneficiaries()) {
+ beneficiary.setEntityId(nextEntityId.getAndIncrement());
+ }
+
+ newAccount.setEntityId(nextEntityId.getAndIncrement());
+ accountsById.put(newAccount.getEntityId(), newAccount);
+ return newAccount;
+ }
+
+ @Override
+ public void update(Account account) {
+ accountsById.put(account.getEntityId(), account);
+ }
+
+ @Override
+ public void updateBeneficiaryAllocationPercentages(Long accountId, Map allocationPercentages) {
+ Account account = accountsById.get(accountId);
+ for (Entry entry : allocationPercentages.entrySet()) {
+ account.getBeneficiary(entry.getKey()).setAllocationPercentage(entry.getValue());
+ }
+ }
+
+ @Override
+ public void addBeneficiary(Long accountId, String beneficiaryName) {
+ accountsById.get(accountId).addBeneficiary(beneficiaryName, Percentage.zero());
+ }
+
+ @Override
+ public void removeBeneficiary(Long accountId, String beneficiaryName,
+ Map allocationPercentages) {
+ accountsById.get(accountId).removeBeneficiary(beneficiaryName);
+ updateBeneficiaryAllocationPercentages(accountId, allocationPercentages);
+ }
+
+}
diff --git a/lab/01-rewards-db/src/main/java/accounts/internal/package.html b/lab/01-rewards-db/src/main/java/accounts/internal/package.html
new file mode 100644
index 0000000..d928cd8
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/accounts/internal/package.html
@@ -0,0 +1,8 @@
+
+
+
The Account data management module for the MVC and later labs.
+ These labs only manage account data and use an AccountManager service
+ instead of a RewardNetwork and AccountRepository. The same underlying
+ database tables are used - but rewards and restaurants are ignored.
The Account module for the MVC and later labs. These labs only
+ manage account data and use different definitions for Account and
+ Beneficiaries to the other labs.
+ * Because this is used by many similar lab projects with slightly different
+ * classes and packages, everything is explicitly created using @Bean methods.
+ * Component-scanning risks picking up unwanted beans in the same package in
+ * other projects.
+ */
+@Configuration
+public class AppConfig {
+
+ @Bean
+ public AccountManager accountManager() {
+ return new JpaAccountManager();
+ }
+
+ @Bean
+ public AccountRepository accountRepository() {
+ return new JpaAccountRepository();
+ }
+
+ @Bean
+ public RestaurantRepository restaurantRepository() {
+ return new JpaRestaurantRepository();
+ }
+
+ @Bean
+ public RewardRepository rewardRepository(DataSource dataSource) {
+ return new JdbcRewardRepository(dataSource);
+ }
+
+}
diff --git a/lab/01-rewards-db/src/main/java/config/DbConfig.java b/lab/01-rewards-db/src/main/java/config/DbConfig.java
new file mode 100644
index 0000000..f006a58
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/config/DbConfig.java
@@ -0,0 +1,106 @@
+package config;
+
+import java.util.Properties;
+import java.util.logging.Logger;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.context.annotation.PropertySource;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.JpaVendorAdapter;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.AbstractJpaVendorAdapter;
+import org.springframework.orm.jpa.vendor.Database;
+import org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.transaction.PlatformTransactionManager;
+
+/**
+ * Configuration class for Persistence-specific objects, including profile
+ * choices for JPA via Hibernate or JPA via EclipseLink. Only used by tests in
+ * this class (since Spring Boot cannot be assumed).
+ *
+ * To simulate Spring Boot we load application.properties manually,
+ * if it exists, and mimic Boot's spring.jpa.show-sql property.
+ */
+@Configuration
+@PropertySource(value = "application.properties", ignoreResourceNotFound = true)
+public class DbConfig {
+
+ public static final String DOMAIN_OBJECTS_PARENT_PACKAGE = "rewards.internal";
+
+ @Value("${spring.jpa.show-sql:true}") // Default to true if not set elsewhere
+ private String showSql;
+
+ /**
+ * Creates an in-memory "rewards" database populated with test data for fast
+ * testing
+ */
+ @Bean
+ public DataSource dataSource() {
+ return (new EmbeddedDatabaseBuilder()) //
+ .addScript("classpath:rewards/testdb/schema.sql") //
+ .addScript("classpath:rewards/testdb/data.sql").build();
+ }
+
+ /**
+ * Transaction Manager For JPA
+ */
+ @Bean
+ public PlatformTransactionManager transactionManager() {
+ return new JpaTransactionManager();
+ }
+
+ /**
+ * Create an EntityManagerFactoryBean.
+ */
+ @Bean
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory(JpaVendorAdapter adapter) {
+
+ // Tell the underlying implementation what type of database we are using - a
+ // hint to generate better SQL
+ if (adapter instanceof AbstractJpaVendorAdapter vendorAdapter) {
+ vendorAdapter.setDatabase(Database.HSQL);
+ }
+
+ // Setup configuration properties
+ Properties props = new Properties();
+ boolean showSql = "TRUE".equalsIgnoreCase(this.showSql);
+ Logger.getLogger("config").info("JPA Show generated SQL? " + this.showSql);
+
+ if (adapter instanceof EclipseLinkJpaVendorAdapter) {
+ props.setProperty("eclipselink.logging.level", showSql ? "FINE" : "WARN");
+ props.setProperty("eclipselink.logging.parameters", String.valueOf(showSql));
+ props.setProperty("eclipselink.weaving", "false");
+ } else {
+ props.setProperty("hibernate.show_sql", String.valueOf(showSql));
+ props.setProperty("hibernate.format_sql", "true");
+ }
+
+ LocalContainerEntityManagerFactoryBean emfb = new LocalContainerEntityManagerFactoryBean();
+ emfb.setPackagesToScan(DOMAIN_OBJECTS_PARENT_PACKAGE);
+ emfb.setJpaProperties(props);
+ emfb.setJpaVendorAdapter(adapter);
+ emfb.setDataSource(dataSource());
+
+ return emfb;
+ }
+
+ @Bean
+ @Profile("!jpa-elink") // Default is JPA using Hibernate
+ JpaVendorAdapter hibernateVendorAdapter() {
+ return new HibernateJpaVendorAdapter();
+ }
+
+ @Bean
+ @Profile("jpa-elink") // Explicitly request JPA using EclipseLink
+ JpaVendorAdapter eclipseLinkVendorAdapter() {
+ return new EclipseLinkJpaVendorAdapter();
+ }
+
+}
diff --git a/lab/01-rewards-db/src/main/java/rewards/AccountContribution.java b/lab/01-rewards-db/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..94774dd
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,140 @@
+package rewards;
+
+import java.io.Serializable;
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+@SuppressWarnings("serial")
+public class AccountContribution implements Serializable {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution implements Serializable {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/Dining.java b/lab/01-rewards-db/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..e656eef
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/Dining.java
@@ -0,0 +1,116 @@
+package rewards;
+
+import java.io.Serializable;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+@SuppressWarnings("serial")
+public class Dining implements Serializable {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/RewardConfirmation.java b/lab/01-rewards-db/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..d04184d
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,44 @@
+package rewards;
+
+import java.io.Serializable;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+@SuppressWarnings("serial")
+public class RewardConfirmation implements Serializable {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/account/Account.java b/lab/01-rewards-db/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..e9984a4
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,290 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+import jakarta.persistence.CascadeType;
+import jakarta.persistence.Column;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
+import jakarta.persistence.OneToMany;
+import jakarta.persistence.Table;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * An account for a member of the reward network. An account has one or more
+ * beneficiaries whose allocations must add up to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is
+ * distributed among the beneficiaries based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+@Entity
+@Table(name = "T_ACCOUNT")
+public class Account {
+
+ @Id
+ @Column(name = "ID")
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long entityId;
+
+ @Column(name = "NUMBER")
+ private String number;
+
+ @Column(name = "NAME")
+ private String name;
+
+ @OneToMany(cascade = CascadeType.ALL)
+ @JoinColumn(name = "ACCOUNT_ID")
+ private Set beneficiaries = new HashSet<>();
+
+ protected Account() {
+ }
+
+ /**
+ * Create a new account.
+ *
+ * @param number
+ * the account number
+ * @param name
+ * the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the entity identifier used to internally distinguish this entity
+ * among other entities of the same type in the system. Should typically
+ * only be called by privileged data access infrastructure code such as an
+ * Object Relational Mapper (ORM) and not by application code.
+ *
+ * @return the internal entity identifier
+ */
+ public Long getEntityId() {
+ return entityId;
+ }
+
+ /**
+ * Sets the internal entity identifier - should only be called by privileged
+ * data access code (repositories that work with an Object Relational Mapper
+ * (ORM)). Should never be set by application code explicitly.
+ *
+ * @param entityId
+ * the internal entity identifier
+ */
+ public void setEntityId(Long entityId) {
+ this.entityId = entityId;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Sets the number used to uniquely identify this account.
+ *
+ * @param number
+ * The number for this account
+ */
+ public void setNumber(String number) {
+ this.number = number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Sets the name on file for this account.
+ *
+ * @param name
+ * The name for this account
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ *
+ * @param beneficiaryName
+ * the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ *
+ * @param beneficiaryName
+ * the name of the beneficiary (should be unique)
+ * @param allocationPercentage
+ * the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName,
+ Percentage allocationPercentage) {
+ beneficiaries
+ .add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Returns the beneficiaries for this account.
+ *
+ * Callers should not attempt to hold on or modify the returned set. This
+ * method should only be used transitively; for example, called to
+ * facilitate account reporting.
+ *
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Returns a single account beneficiary. Callers should not attempt to hold
+ * on or modify the returned object. This method should only be used
+ * transitively; for example, called to facilitate reporting or testing.
+ *
+ * @param name
+ * the name of the beneficiary e.g "Annabelle"
+ * @return the beneficiary object
+ */
+ public Beneficiary getBeneficiary(String name) {
+ for (Beneficiary b : beneficiaries) {
+ if (b.getName().equals(name)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("No such beneficiary with name '"
+ + name + "'");
+ }
+
+ /**
+ * Removes a single beneficiary from this account.
+ *
+ * @param beneficiaryName
+ * the name of the beneficiary (should be unique)
+ */
+ public void removeBeneficiary(String beneficiaryName) {
+ beneficiaries.remove(getBeneficiary(beneficiaryName));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary
+ * allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ try {
+ totalPercentage = totalPercentage.add(b
+ .getAllocationPercentage());
+ } catch (IllegalArgumentException e) {
+ // total would have been over 100% - return invalid
+ return false;
+ }
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ public void setValid(boolean valid) {
+ // DO NOTHING. Needed for JSON processing on client.
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is
+ * distributed among the account's beneficiaries based on each beneficiary's
+ * allocation percentage.
+ *
+ * @param amount
+ * the total amount to contribute
+ * @param contribution
+ * the contribution summary
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ *
+ * @param amount
+ * the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(
+ beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary
+ .getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(),
+ distributionAmount, beneficiary.getAllocationPercentage(),
+ beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the
+ * repository responsible for reconstituting this account.
+ *
+ * @param beneficiary
+ * the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ /**
+ * String representation for debugging.
+ */
+ public String toString() {
+ return entityId + ": Number = '" + number + "', name = " + name
+ + "', beneficiaries = " + beneficiaries;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Account account = (Account) o;
+ return Objects.equals(entityId, account.entityId) &&
+ Objects.equals(number, account.number) &&
+ Objects.equals(name, account.name) &&
+ Objects.equals(beneficiaries, account.beneficiaries);
+ }
+
+ @Override
+ public int hashCode() {
+
+ return Objects.hash(entityId, number, name, beneficiaries);
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/account/AccountRepository.java b/lab/01-rewards-db/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..fd3d23b
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,26 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Indicates implementation being used. Actual implementation is hidden
+ * behind a proxy, so this makes it easy to determine when testing.
+ *
+ * @return Implementation information.
+ */
+ String getInfo();
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/account/Beneficiary.java b/lab/01-rewards-db/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..9b761be
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,146 @@
+package rewards.internal.account;
+
+import jakarta.persistence.AttributeOverride;
+import jakarta.persistence.Column;
+import jakarta.persistence.Embedded;
+import jakarta.persistence.Entity;
+import jakarta.persistence.GeneratedValue;
+import jakarta.persistence.GenerationType;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name
+ * (e.g. Mary), an allocation percentage and a savings balance tracking how much
+ * money has been saved for them to date (e.g. $1000).
+ */
+@Entity
+@Table(name = "T_ACCOUNT_BENEFICIARY")
+public class Beneficiary {
+
+ @Id
+ @Column(name = "ID")
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ private Long entityId;
+
+ @Column(name = "NAME")
+ private String name;
+
+ @Embedded
+ @AttributeOverride(name = "value", column = @Column(name = "ALLOCATION_PERCENTAGE"))
+ private Percentage allocationPercentage;
+
+ @Embedded
+ @AttributeOverride(name = "value", column = @Column(name = "SAVINGS"))
+ private MonetaryAmount savings = MonetaryAmount.zero();
+
+ protected Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ *
+ * @param name
+ * the name of the beneficiary
+ * @param allocationPercentage
+ * the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by
+ * privileged objects responsible for reconstituting an existing Account
+ * object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be
+ * called by general application code.
+ *
+ * @param name
+ * the name of the beneficiary
+ * @param allocationPercentage
+ * the beneficiary's allocation percentage within its account
+ * @param savings
+ * the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage,
+ MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the entity identifier used to internally distinguish this entity
+ * among other entities of the same type in the system. Should typically
+ * only be called by privileged data access infrastructure code such as an
+ * Object Relational Mapper (ORM) and not by application code.
+ *
+ * @return the internal entity identifier
+ */
+ public Long getEntityId() {
+ return entityId;
+ }
+
+ /**
+ * Sets the internal entity identifier - should only be called by privileged
+ * data access code (repositories that work with an Object Relational Mapper
+ * (ORM)). Should never be set by application code explicitly.
+ *
+ * @param entityId
+ * the internal entity identifier
+ */
+ public void setEntityId(Long entityId) {
+ this.entityId = entityId;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Sets the beneficiary's allocation percentage in this account.
+ *
+ * @param allocationPercentage
+ * The new allocation percentage
+ */
+ public void setAllocationPercentage(Percentage allocationPercentage) {
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ *
+ * @param amount
+ * the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "' (" + entityId + "), allocationPercentage = "
+ + allocationPercentage + ", savings = " + savings;
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/account/JpaAccountRepository.java b/lab/01-rewards-db/src/main/java/rewards/internal/account/JpaAccountRepository.java
new file mode 100644
index 0000000..dcc4914
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/account/JpaAccountRepository.java
@@ -0,0 +1,51 @@
+package rewards.internal.account;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * An account repository that uses JPA to find accounts.
+ */
+public class JpaAccountRepository implements AccountRepository {
+
+ public static final String ACCOUNT_BY_CC_QUERY = "select ACCOUNT_ID from T_ACCOUNT_CREDIT_CARD where NUMBER = :ccn";
+
+ public static final String INFO = "JPA";
+
+ private static final Logger logger = LoggerFactory.getLogger("config");
+
+ private EntityManager entityManager;
+
+ public JpaAccountRepository() {
+ logger.info("Created JpaAccountManager");
+ }
+
+ @PersistenceContext
+ public void setEntityManager(EntityManager entityManager) {
+ this.entityManager = entityManager;
+ }
+
+ @Override
+ public String getInfo() {
+ return INFO;
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ // Find id account of account with this credit-card using a direct
+ // SQL query on the unmapped T_ACCOUNT_CREDIT_CARD table.
+ Integer accountId = (Integer) entityManager
+ .createNativeQuery(ACCOUNT_BY_CC_QUERY)
+ .setParameter("ccn", creditCardNumber).getSingleResult();
+
+ Account account = entityManager.find(Account.class, accountId.longValue());
+
+ // Force beneficiaries to load too - avoid Hibernate lazy loading error
+ account.getBeneficiaries().size();
+
+ return account;
+ }
+
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/account/StubAccountRepository.java b/lab/01-rewards-db/src/main/java/rewards/internal/account/StubAccountRepository.java
new file mode 100644
index 0000000..7437a5e
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/account/StubAccountRepository.java
@@ -0,0 +1,48 @@
+package rewards.internal.account;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.orm.ObjectRetrievalFailureException;
+
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ public static final String TYPE = "Stub";
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ @Override
+ public String getInfo() {
+ return TYPE;
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new ObjectRetrievalFailureException(Account.class, creditCardNumber);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/account/package.html b/lab/01-rewards-db/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Account module.
+
+
+
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/AlwaysAvailable.java b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/AlwaysAvailable.java
new file mode 100644
index 0000000..f97fbeb
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/AlwaysAvailable.java
@@ -0,0 +1,19 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * A benefit availabilty policy that returns true at all times.
+ */
+public class AlwaysAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new AlwaysAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+
+ public String toString() {
+ return "alwaysAvailable";
+ }
+}
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
new file mode 100644
index 0000000..b7d6d74
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
@@ -0,0 +1,20 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * Determines if benefit is available for an account for dining.
+ *
+ * A value object. A strategy. Scoped by the Resturant aggregate.
+ */
+public interface BenefitAvailabilityPolicy {
+
+ /**
+ * Calculates if an account is eligible to receive benefits for a dining.
+ * @param account the account of the member who dined
+ * @param dining the dining event
+ * @return benefit availability status
+ */
+ boolean isBenefitAvailableFor(Account account, Dining dining);
+}
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/JpaRestaurantRepository.java b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/JpaRestaurantRepository.java
new file mode 100644
index 0000000..835c58c
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/JpaRestaurantRepository.java
@@ -0,0 +1,56 @@
+package rewards.internal.restaurant;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaQuery;
+
+/**
+ * Loads restaurants from a data source using JPA.
+ */
+public class JpaRestaurantRepository implements RestaurantRepository {
+
+ public static final String RESTAURANT_BY_MERCHANT_QUERY = //
+ "select r from Restaurant r where r.number = :merchantNumber";
+
+ public static final String INFO = "JPA";
+
+ private static final Logger logger = LoggerFactory.getLogger("config");
+
+ private EntityManager entityManager;
+
+ public JpaRestaurantRepository() {
+ logger.info("Created JpaRestaurantRepository");
+ }
+
+ @PersistenceContext
+ public void setEntityManager(EntityManager entityManager) {
+ this.entityManager = entityManager;
+ }
+
+ @Override
+ public String getInfo() {
+ return INFO;
+ }
+
+ @Override
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ return entityManager //
+ .createQuery(RESTAURANT_BY_MERCHANT_QUERY, Restaurant.class) //
+ .setParameter("merchantNumber", merchantNumber) //
+ .getSingleResult();
+ }
+
+ @Override
+ public Long getRestaurantCount() {
+ CriteriaBuilder qb = entityManager.getCriteriaBuilder();
+
+ CriteriaQuery cq = qb.createQuery(Long.class);
+ cq.select(qb.count(cq.from(Restaurant.class)));
+
+ return entityManager.createQuery(cq).getSingleResult();
+ }
+}
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/NeverAvailable.java b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/NeverAvailable.java
new file mode 100644
index 0000000..8f6cbc3
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/NeverAvailable.java
@@ -0,0 +1,19 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * A benefit availabilty policy that returns false at all times.
+ */
+public class NeverAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new NeverAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return false;
+ }
+
+ public String toString() {
+ return "neverAvailable";
+ }
+}
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..20d3597
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,207 @@
+package rewards.internal.restaurant;
+
+import jakarta.persistence.Access;
+import jakarta.persistence.AccessType;
+import jakarta.persistence.AttributeOverride;
+import jakarta.persistence.Column;
+import jakarta.persistence.Embedded;
+import jakarta.persistence.Entity;
+import jakarta.persistence.Id;
+import jakarta.persistence.Table;
+import jakarta.persistence.Transient;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for
+ * dining based on an availability policy and a benefit percentage.
+ */
+@Entity
+@Table(name = "T_RESTAURANT")
+public class Restaurant {
+
+ @Id
+ @Column(name = "ID")
+ private Long entityId;
+
+ @Column(name = "MERCHANT_NUMBER")
+ private String number;
+
+ @Column(name = "NAME")
+ private String name;
+
+ @Embedded
+ @AttributeOverride(name = "value", column = @Column(name="BENEFIT_PERCENTAGE"))
+ private Percentage benefitPercentage;
+
+ /**
+ * This class needs special mapping via its own accessor methods - see:
+ *
+ *
{@link #getDbBenefitAvailabilityPolicy()}
+ *
{@link #setDbBenefitAvailabilityPolicy(String)}
+ *
+ * It is marked transient to hide it from the default field mapping
+ * mechanism.
+ */
+ @Transient
+ //@Column(name = "BENEFIT_AVAILABILITY_POLICY")
+ private BenefitAvailabilityPolicy benefitAvailabilityPolicy;
+
+ protected Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ *
+ * @param number
+ * the restaurant's merchant number
+ * @param name
+ * the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the entity identifier used to internally distinguish this entity
+ * among other entities of the same type in the system. Should typically
+ * only be called by privileged data access infrastructure code such as an
+ * Object Relational Mapper (ORM) and not by application code.
+ *
+ * @return the internal entity identifier
+ */
+ protected Long getEntityId() {
+ return entityId;
+ }
+
+ /**
+ * Sets the internal entity identifier - should only be called by privileged
+ * data access code (repositories that work with an Object Relational Mapper
+ * (ORM)). Should never be set by application code explicitly.
+ *
+ * @param entityId
+ * the internal entity identifier
+ */
+ public void setEntityId(Long entityId) {
+ this.entityId = entityId;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining
+ * transactions.
+ *
+ * @param benefitPercentage
+ * the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Sets the policy that determines if a dining by an account at this
+ * restaurant is eligible for benefit.
+ *
+ * @param benefitAvailabilityPolicy
+ * the benefit availability policy
+ */
+ public void setBenefitAvailabilityPolicy(
+ BenefitAvailabilityPolicy benefitAvailabilityPolicy) {
+ this.benefitAvailabilityPolicy = benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Returns this restaurant's benefit availability policy.
+ */
+ public BenefitAvailabilityPolicy getBenefitAvailabilityPolicy() {
+ return benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this
+ * restaurant.
+ *
+ * @param account
+ * the account that dined at this restaurant
+ * @param dining
+ * a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ if (benefitAvailabilityPolicy.isBenefitAvailableFor(account, dining)) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ } else {
+ return MonetaryAmount.zero();
+ }
+ }
+
+ // Internal methods for JPA only - hence they are protected.
+ /**
+ * Sets this restaurant's benefit availability policy from the code stored
+ * in the underlying column. This method is a database specific accessor
+ * using the JPA 2 @Access annotation.
+ */
+ @Access(AccessType.PROPERTY)
+ @Column(name = "BENEFIT_AVAILABILITY_POLICY")
+ protected void setDbBenefitAvailabilityPolicy(String policyCode) {
+ if ("A".equals(policyCode)) {
+ benefitAvailabilityPolicy = AlwaysAvailable.INSTANCE;
+ } else if ("N".equals(policyCode)) {
+ benefitAvailabilityPolicy = NeverAvailable.INSTANCE;
+ } else {
+ throw new IllegalArgumentException("Not a supported policy code "
+ + policyCode);
+ }
+ }
+
+ /**
+ * Returns this restaurant's benefit availability policy code for storage in
+ * the underlying column. This method is a database specific accessor using
+ * the JPA 2 @Access annotation.
+ */
+ @Access(AccessType.PROPERTY)
+ @Column(name = "BENEFIT_AVAILABILITY_POLICY")
+ protected String getDbBenefitAvailabilityPolicy() {
+ if (benefitAvailabilityPolicy == AlwaysAvailable.INSTANCE) {
+ return "A";
+ } else if (benefitAvailabilityPolicy == NeverAvailable.INSTANCE) {
+ return "N";
+ } else {
+ throw new IllegalArgumentException("No policy code for "
+ + benefitAvailabilityPolicy.getClass());
+ }
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name
+ + "', benefitPercentage = " + benefitPercentage
+ + ", benefitAvailabilityPolicy = " + benefitAvailabilityPolicy;
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..2dbdff0
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,36 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and
+ * reconstitute Restaurant entities from an external form such as a set of RDMS
+ * rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized
+ * and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Indicates implementation being used. Actual implementation is hidden
+ * behind a proxy, so this makes it easy to determine when testing.
+ *
+ * @return Implementation information.
+ */
+ String getInfo();
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ *
+ * @param merchantNumber
+ * the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+
+ /**
+ * Find the number of restaurants in the repository.
+ *
+ * @return The number of restaurants - zero or more.
+ */
+ Long getRestaurantCount();
+}
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/StubRestaurantRepository.java b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/StubRestaurantRepository.java
new file mode 100644
index 0000000..14c236c
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/StubRestaurantRepository.java
@@ -0,0 +1,63 @@
+package rewards.internal.restaurant;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.orm.ObjectRetrievalFailureException;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ public static final String TYPE = "Stub";
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new AlwaysReturnsTrue());
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ @Override
+ public String getInfo() {
+ return TYPE;
+ }
+
+ @Override
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new ObjectRetrievalFailureException(Restaurant.class, merchantNumber);
+ }
+ return restaurant;
+ }
+
+ @Override
+ public Long getRestaurantCount() {
+ return 1L;
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy that always returns true. Only useful for testing--a real
+ * availability policy might consider many factors such as the day of week of the dining, or the account's reward
+ * history for the current month.
+ */
+ private static class AlwaysReturnsTrue implements BenefitAvailabilityPolicy {
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/package.html b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/01-rewards-db/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..8ac9658
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,50 @@
+package rewards.internal.reward;
+
+import javax.sql.DataSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import common.datetime.SimpleDate;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * JDBC implementation of a reward repository that records the result of a
+ * reward transaction by inserting a reward confirmation record.
+ */
+public class JdbcRewardRepository implements RewardRepository {
+
+ public static final String TYPE = "jdbc";
+
+ private static final Logger logger = LoggerFactory.getLogger("config");
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public JdbcRewardRepository(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ logger.info("Created JdbcRewardRepository");
+ }
+
+ @Override
+ public String getInfo() {
+ return TYPE;
+ }
+
+ @Override
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ String confirmationNumber = nextConfirmationNumber();
+ jdbcTemplate.update(sql, confirmationNumber, contribution.getAmount().asBigDecimal(),
+ SimpleDate.today().asDate(), contribution.getAccountNumber(), dining.getMerchantNumber(),
+ dining.getDate().asDate(), dining.getAmount().asBigDecimal());
+ return new RewardConfirmation(confirmationNumber, contribution);
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ return jdbcTemplate.queryForObject(sql, String.class);
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/01-rewards-db/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..1dd257c
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,28 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Indicates implementation being used. Actual implementation is hidden
+ * behind a proxy, so this makes it easy to determine when testing.
+ *
+ * @return Implementation information.
+ */
+ String getInfo();
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/reward/StubRewardRepository.java b/lab/01-rewards-db/src/main/java/rewards/internal/reward/StubRewardRepository.java
new file mode 100644
index 0000000..2f89f16
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/reward/StubRewardRepository.java
@@ -0,0 +1,37 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * A dummy reward repository implementation.
+ *
+ * IMPORTANT!!! Per best practices, this class shouldn't be in 'src/main/java'
+ * but rather in 'src/test/java'. However, it is used by numerous Test classes
+ * inside multiple projects. Maven does not provide an easy way to access a
+ * class that is inside another project's 'src/test/java' folder.
+ *
+ * Rather than using some complex Maven configuration, we decided it is
+ * acceptable to place this test class inside 'src/main/java'.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public static final String TYPE = "Stub";
+
+ int nextConfirmationNumber = 0;
+
+ @Override
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ @Override
+ public String getInfo() {
+ return TYPE;
+ }
+
+ private String confirmationNumber() {
+ return String.valueOf(nextConfirmationNumber++);
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/main/java/rewards/internal/reward/package.html b/lab/01-rewards-db/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/01-rewards-db/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+ * Tests application behavior to verify the AccountManager and the database
+ * mapping of its domain objects are correct. The implementation of the
+ * AccountManager class is irrelevant to these tests and so is the testing
+ * environment (stubbing, manual or Spring-driven configuration).
+ */
+public abstract class AbstractAccountManagerTests {
+
+ protected final Logger logger;
+
+ @Autowired
+ protected AccountManager accountManager;
+
+ public AbstractAccountManagerTests() {
+ logger = LoggerFactory.getLogger(getClass());
+ if (logger instanceof ch.qos.logback.classic.Logger logger1)
+ logger1.setLevel(Level.INFO);
+ }
+
+ /**
+ * Quick test to check the right profile is being used.
+ */
+ @Test
+ public abstract void testProfile();
+
+ /**
+ * How many accounts are defined for use in the test?
+ *
+ * @return Number of accounts available.
+ */
+ protected abstract int getNumAccountsExpected();
+
+ /**
+ * Used to log current transactional status.
+ */
+ protected abstract void showStatus();
+
+ @Test
+ @Transactional
+ public void testGetAllAccounts() {
+ showStatus();
+ List accounts = accountManager.getAllAccounts();
+ assertEquals(getNumAccountsExpected(), accounts.size(), "Wrong number of accounts");
+ }
+
+ @Test
+ @Transactional
+ public void getAccount() {
+ Account account = accountManager.getAccount(0L);
+ // assert the returned account contains what you expect given the state
+ // of the database
+ assertNotNull(account, "account should never be null");
+ assertEquals(account.getEntityId().longValue(), 0L, "wrong entity id");
+ assertEquals("123456789", account.getNumber(), "wrong account number");
+ assertEquals("Keith and Keri Donald", account.getName(), "wrong name");
+ assertEquals(2, account.getBeneficiaries().size(), "wrong beneficiary collection size");
+
+ Beneficiary b1 = account.getBeneficiary("Annabelle");
+ assertNotNull(b1, "Annabelle should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b1.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b1.getAllocationPercentage(), "wrong allocation percentage");
+
+ Beneficiary b2 = account.getBeneficiary("Corgan");
+ assertNotNull(b2, "Corgan should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b2.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b2.getAllocationPercentage(), "wrong allocation percentage");
+ }
+
+ @Test
+ @Transactional
+ public void addAccount() {
+ // FIX ME: - Manual configuration fails, Spring driven integration tests work
+ // OK.
+ if (accountManager instanceof JpaAccountManager
+ && this.getClass().getAnnotation(ContextConfiguration.class) == null)
+ return;
+
+ // long newAccountId = getNumAccountsExpected();
+
+ Account account = new Account("1010101", "Test");
+ account.addBeneficiary("Bene1", Percentage.valueOf("100%"));
+
+ showStatus();
+ Account newAccount = accountManager.save(account);
+
+ assertEquals(getNumAccountsExpected() + 1, accountManager.getAllAccounts().size(), "Wrong number of accounts");
+
+ newAccount = accountManager.getAccount(newAccount.getEntityId());
+ assertNotNull(newAccount, "Did not find new account");
+ assertEquals("Test", newAccount.getName(), "Did not save account");
+ assertEquals(1, newAccount.getBeneficiaries().size(), "Did not save beneficiary");
+ }
+
+ @Test
+ @Transactional
+ public void updateAccount() {
+ Account oldAccount = accountManager.getAccount(0L);
+ oldAccount.setName("Ben Hale");
+ accountManager.update(oldAccount);
+ Account newAccount = accountManager.getAccount(0L);
+ assertEquals("Ben Hale", newAccount.getName(), "Did not persist the name change");
+ }
+
+ @Test
+ @Transactional
+ public void updateAccountBeneficiaries() {
+ Map allocationPercentages = new HashMap<>();
+ allocationPercentages.put("Annabelle", Percentage.valueOf("25%"));
+ allocationPercentages.put("Corgan", Percentage.valueOf("75%"));
+ accountManager.updateBeneficiaryAllocationPercentages(0L, allocationPercentages);
+ Account account = accountManager.getAccount(0L);
+ assertEquals(Percentage.valueOf("25%"),
+ account.getBeneficiary("Annabelle").getAllocationPercentage(), "Invalid adjusted percentage");
+ assertEquals(Percentage.valueOf("75%"),
+ account.getBeneficiary("Corgan").getAllocationPercentage(), "Invalid adjusted percentage");
+ }
+
+ @Test
+ @Transactional
+ public void addBeneficiary() {
+ accountManager.addBeneficiary(0L, "Ben");
+ Account account = accountManager.getAccount(0L);
+ assertEquals( 3, account.getBeneficiaries().size(), "Should only have three beneficiaries");
+ }
+
+ @Test
+ @Transactional
+ public void removeBeneficiary() {
+ Map allocationPercentages = new HashMap<>();
+ allocationPercentages.put("Corgan", Percentage.oneHundred());
+ accountManager.removeBeneficiary(0L, "Annabelle", allocationPercentages);
+ Account account = accountManager.getAccount(0L);
+ assertEquals(1, account.getBeneficiaries().size(), "Should only have one beneficiary");
+ assertEquals(Percentage.oneHundred(),
+ account.getBeneficiary("Corgan").getAllocationPercentage(), "Corgan should now have 100% allocation");
+ }
+
+}
diff --git a/lab/01-rewards-db/src/test/java/accounts/internal/AbstractDatabaseAccountManagerTests.java b/lab/01-rewards-db/src/test/java/accounts/internal/AbstractDatabaseAccountManagerTests.java
new file mode 100644
index 0000000..0d88a27
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/accounts/internal/AbstractDatabaseAccountManagerTests.java
@@ -0,0 +1,55 @@
+package accounts.internal;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.transaction.PlatformTransactionManager;
+import utils.TransactionUtils;
+
+import javax.sql.DataSource;
+
+/**
+ * Supports transactional testing of AccountManager implementation in both a
+ * manual and a Spring-configured environment.
+ *
+ * Manual configuration, allows testing of an AccountManager implementation
+ * without Spring.
+ *
+ * Automated configuration using a class annotated with @ContextConfiguration
+ * tests both the implementation of AccountManager and the Spring configuration
+ * files.
+ */
+public abstract class AbstractDatabaseAccountManagerTests extends AbstractAccountManagerTests {
+
+ protected static int numAccountsInDb = -1;
+
+ @Autowired
+ protected PlatformTransactionManager transactionManager;
+
+ @Autowired
+ protected DataSource dataSource;
+
+ protected TransactionUtils transactionUtils;
+
+ @Override
+ protected void showStatus() {
+ logger.info("TRANSACTION IS : " + transactionUtils.getCurrentTransaction());
+ }
+
+ @BeforeEach
+ public void setUp() throws Exception {
+ // The number of test accounts in the database - a static variable so we only do
+ // this once.
+ if (numAccountsInDb == -1)
+ numAccountsInDb = new JdbcTemplate(dataSource).queryForObject("SELECT count(*) FROM T_Account",
+ Integer.class);
+
+ // Setup the transaction utility class
+ transactionUtils = new TransactionUtils(transactionManager);
+ }
+
+ @Override
+ protected int getNumAccountsExpected() {
+ return numAccountsInDb;
+ }
+}
diff --git a/lab/01-rewards-db/src/test/java/accounts/internal/AccountManagerTests.java b/lab/01-rewards-db/src/test/java/accounts/internal/AccountManagerTests.java
new file mode 100644
index 0000000..ed1bb7e
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/accounts/internal/AccountManagerTests.java
@@ -0,0 +1,22 @@
+package accounts.internal;
+
+public class AccountManagerTests extends AbstractAccountManagerTests {
+
+ public AccountManagerTests() {
+ accountManager = new StubAccountManager();
+ }
+
+ @Override
+ public void testProfile() {
+ }
+
+ @Override
+ protected int getNumAccountsExpected() {
+ return StubAccountManager.NUM_ACCOUNTS_IN_STUB;
+ }
+
+ @Override
+ protected void showStatus() {
+ }
+
+}
diff --git a/lab/01-rewards-db/src/test/java/accounts/internal/JpaAccountManagerIntegrationTests.java b/lab/01-rewards-db/src/test/java/accounts/internal/JpaAccountManagerIntegrationTests.java
new file mode 100644
index 0000000..d11ff63
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/accounts/internal/JpaAccountManagerIntegrationTests.java
@@ -0,0 +1,30 @@
+package accounts.internal;
+
+import config.AppConfig;
+import config.DbConfig;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Spring-driven integration test for the JPA-based account manager
+ * implementation. Verifies that the JpaAccountManager works with its underlying
+ * components.
+ */
+@ActiveProfiles("jpa")
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = { AppConfig.class, DbConfig.class })
+public class JpaAccountManagerIntegrationTests extends AbstractDatabaseAccountManagerTests {
+
+ @Test
+ @Override
+ public void testProfile() {
+ assertEquals("JPA", accountManager.getInfo(), "JPA expected but found " + accountManager.getInfo());
+ }
+
+}
diff --git a/lab/01-rewards-db/src/test/java/accounts/internal/JpaAccountManagerManualIntegrationTests.java b/lab/01-rewards-db/src/test/java/accounts/internal/JpaAccountManagerManualIntegrationTests.java
new file mode 100644
index 0000000..aa4ae74
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/accounts/internal/JpaAccountManagerManualIntegrationTests.java
@@ -0,0 +1,51 @@
+package accounts.internal;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import utils.DataManagementSetup;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Manually configured integration test (not using Spring) for the JPA-based
+ * account manager implementation. Verifies that the JpaAccountManager works
+ * with its underlying components.
+ */
+public class JpaAccountManagerManualIntegrationTests extends AbstractDatabaseAccountManagerTests {
+
+ final DataManagementSetup dataManagementSetup = new DataManagementSetup();
+
+ public JpaAccountManagerManualIntegrationTests() {
+ setupForTest();
+ }
+
+ @Test
+ @Override
+ public void testProfile() {
+ assertTrue(accountManager instanceof JpaAccountManager, "JPA expected");
+ logger.info("JPA with Hibernate");
+ }
+
+ @BeforeEach
+ @Override
+ public void setUp() throws Exception {
+ super.setUp();
+ transactionUtils.beginTransaction();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ transactionUtils.rollbackTransaction();
+ }
+
+ private void setupForTest() {
+ dataSource = dataManagementSetup.getDataSource();
+
+ JpaAccountManager accountManager = new JpaAccountManager();
+ accountManager.setEntityManager(dataManagementSetup.createEntityManager());
+ this.accountManager = accountManager;
+ transactionManager = dataManagementSetup.getTransactionManager();
+ }
+
+}
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/account/AbstractAccountRepositoryTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/account/AbstractAccountRepositoryTests.java
new file mode 100644
index 0000000..aff0887
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/account/AbstractAccountRepositoryTests.java
@@ -0,0 +1,57 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+/**
+ * Unit tests for an account repository implementation.
+ *
+ * Tests application behavior to verify the AccountRepository and the database
+ * mapping of its domain objects are correct. The implementation of the
+ * AccountRepository class is irrelevant to these tests and so is the testing
+ * environment (stubbing, manual or Spring-driven configuration).
+ */
+public abstract class AbstractAccountRepositoryTests {
+
+ @Autowired
+ protected AccountRepository accountRepository;
+
+ @Test
+ public abstract void testProfile();
+
+ @Test
+ @Transactional
+ public void findByCreditCard() {
+ Account account = accountRepository
+ .findByCreditCard("1234123412341234");
+
+ // assert the returned account contains what you expect given the state
+ // of the database
+ assertNotNull(account, "account should never be null");
+ assertEquals(Long.valueOf(0), account.getEntityId(), "wrong entity id");
+ assertEquals("123456789", account.getNumber(), "wrong account number");
+ assertEquals("Keith and Keri Donald", account.getName(), "wrong name");
+ assertEquals(2, account
+ .getBeneficiaries().size(), "wrong beneficiary collection size");
+
+ Beneficiary b1 = account.getBeneficiary("Annabelle");
+ assertNotNull(b1, "Annabelle should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"),
+ b1.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"),
+ b1.getAllocationPercentage(), "wrong allocation percentage");
+
+ Beneficiary b2 = account.getBeneficiary("Corgan");
+ assertNotNull(b2, "Corgan should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"),
+ b2.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"),
+ b2.getAllocationPercentage(), "wrong allocation percentage");
+ }
+}
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/account/AccountTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/account/AccountTests.java
new file mode 100644
index 0000000..d2a53ae
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/account/AccountTests.java
@@ -0,0 +1,53 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.Test;
+import rewards.AccountContribution;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the Account class that verify Account behavior works in isolation.
+ */
+public class AccountTests {
+
+ private final Account account = new Account("1", "Keith and Keri Donald");
+
+ @Test
+ public void accountIsValid() {
+ // setup account with a valid set of beneficiaries to prepare for testing
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ assertTrue(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWithNoBeneficiaries() {
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreOver100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreUnder100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("25%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void makeContribution() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ assertEquals(contribution.getAmount(), MonetaryAmount.valueOf("100.00"));
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/account/JpaAccountRepositoryIntegrationTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/account/JpaAccountRepositoryIntegrationTests.java
new file mode 100644
index 0000000..a2fbe07
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/account/JpaAccountRepositoryIntegrationTests.java
@@ -0,0 +1,30 @@
+package rewards.internal.account;
+
+import config.AppConfig;
+import config.DbConfig;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+/**
+ * Integration test for the JPA based account repository implementation.
+ * Verifies that the JpaAccountRepository works with its underlying components
+ * and that Spring is configuring things properly.
+ */
+@ActiveProfiles("jpa")
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = { AppConfig.class, DbConfig.class })
+public class JpaAccountRepositoryIntegrationTests extends AbstractAccountRepositoryTests {
+
+ @Test
+ @Override
+ public void testProfile() {
+ assertEquals(JpaAccountRepository.INFO, accountRepository.getInfo(), "JPA expected but found " + accountRepository.getInfo());
+ }
+
+}
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/account/JpaAccountRepositoryTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/account/JpaAccountRepositoryTests.java
new file mode 100644
index 0000000..fddd4e9
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/account/JpaAccountRepositoryTests.java
@@ -0,0 +1,49 @@
+package rewards.internal.account;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+import utils.DataManagementSetup;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Manually configured integration test for the JPA based account repository
+ * implementation.Tests repository behavior and verifies the Account JPA mapping
+ * is correct.
+ */
+public class JpaAccountRepositoryTests extends AbstractAccountRepositoryTests {
+
+ private PlatformTransactionManager transactionManager;
+
+ private TransactionStatus transactionStatus;
+
+ @BeforeEach
+ public void setUp() {
+ DataManagementSetup dataManagementSetup = new DataManagementSetup();
+
+ JpaAccountRepository accountRepository = new JpaAccountRepository();
+ accountRepository.setEntityManager(dataManagementSetup.createEntityManager());
+ this.accountRepository = accountRepository;
+
+ // begin a transaction
+ transactionManager = dataManagementSetup.getTransactionManager();
+ transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
+ }
+
+ @Test
+ @Override
+ public void testProfile() {
+ assertTrue(accountRepository instanceof JpaAccountRepository, "JPA expected");
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // rollback the transaction to avoid corrupting other tests
+ if (transactionManager != null)
+ transactionManager.rollback(transactionStatus);
+ }
+}
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/AbstractRestaurantRepositoryTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/AbstractRestaurantRepositoryTests.java
new file mode 100644
index 0000000..651b7e8
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/AbstractRestaurantRepositoryTests.java
@@ -0,0 +1,43 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+/**
+ * Unit tests for a restaurant repository implementation.
+ *
+ * Tests application behavior to verify the RestaurantRepository and the
+ * database mapping of its domain objects are correct. The implementation of the
+ * RestaurantRepository class is irrelevant to these tests and so is the testing
+ * environment (stubbing, manual or Spring-driven configuration).
+ */
+public abstract class AbstractRestaurantRepositoryTests {
+
+ @Autowired
+ protected RestaurantRepository restaurantRepository;
+
+ @Test
+ public abstract void testProfile();
+
+ @Test
+ @Transactional
+ public void findRestaurantByMerchantNumber() {
+ Restaurant restaurant = restaurantRepository
+ .findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "the restaurant should never be null");
+ assertEquals("1234567890",
+ restaurant.getNumber(), "the merchant number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "the name is wrong");
+ assertEquals(
+ Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "the benefitPercentage is wrong");
+ assertEquals(
+ AlwaysAvailable.INSTANCE,
+ restaurant.getBenefitAvailabilityPolicy(), "the benefit availability policy is wrong");
+ }
+
+}
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/JpaRestaurantRepositoryIntegrationTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/JpaRestaurantRepositoryIntegrationTests.java
new file mode 100644
index 0000000..267ecdb
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/JpaRestaurantRepositoryIntegrationTests.java
@@ -0,0 +1,30 @@
+package rewards.internal.restaurant;
+
+import config.AppConfig;
+import config.DbConfig;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Integration test for the JPA-based restaurant repository implementation.
+ * Verifies that the JpaRestaurantRepository works with its underlying
+ * components and that Spring is configuring things properly.
+ */
+@ActiveProfiles("jpa")
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = {AppConfig.class, DbConfig.class})
+public class JpaRestaurantRepositoryIntegrationTests extends AbstractRestaurantRepositoryTests {
+
+ @Test
+ @Override
+ public void testProfile() {
+ assertEquals(JpaRestaurantRepository.INFO, restaurantRepository.getInfo(), "JPA expected but found " + restaurantRepository.getInfo());
+ }
+
+}
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/JpaRestaurantRepositoryTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/JpaRestaurantRepositoryTests.java
new file mode 100644
index 0000000..bf2f2ff
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/JpaRestaurantRepositoryTests.java
@@ -0,0 +1,50 @@
+package rewards.internal.restaurant;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+import utils.DataManagementSetup;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Manually configured integration test for the JPA based restaurant repository
+ * implementation. Tests repository behavior and verifies the Restaurant JPA
+ * mapping is correct.
+ */
+public class JpaRestaurantRepositoryTests extends AbstractRestaurantRepositoryTests {
+
+ private PlatformTransactionManager transactionManager;
+
+ private TransactionStatus transactionStatus;
+
+ @BeforeEach
+ public void setUp() {
+ DataManagementSetup dataManagementSetup = new DataManagementSetup();
+
+ JpaRestaurantRepository restaurantRepository = new JpaRestaurantRepository();
+ restaurantRepository.setEntityManager(dataManagementSetup.createEntityManager());
+ this.restaurantRepository = restaurantRepository;
+
+ // begin a transaction
+ transactionManager = dataManagementSetup.getTransactionManager();
+ transactionStatus = transactionManager.getTransaction(new DefaultTransactionDefinition());
+ }
+
+ @Test
+ @Override
+ public void testProfile() {
+ assertTrue(restaurantRepository instanceof JpaRestaurantRepository, "JPA expected");
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // rollback the transaction to avoid corrupting other tests
+ if (transactionManager != null)
+ transactionManager.rollback(transactionStatus);
+ }
+
+}
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/RestaurantTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/RestaurantTests.java
new file mode 100644
index 0000000..450e81c
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/restaurant/RestaurantTests.java
@@ -0,0 +1,69 @@
+package rewards.internal.restaurant;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests for exercising the behavior of the Restaurant aggregate entity. A restaurant calculates a benefit to award
+ * to an account for dining based on an availability policy and benefit percentage.
+ */
+public class RestaurantTests {
+
+ private Restaurant restaurant;
+
+ private Account account;
+
+ private Dining dining;
+
+ @BeforeEach
+ public void setUp() {
+ // configure the restaurant, the object being tested
+ restaurant = new Restaurant("1234567890", "AppleBee's");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(true));
+ // configure supporting objects needed by the restaurant
+ account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle");
+ dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+ }
+
+ @Test
+ public void testCalcuateBenefitFor() {
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert 8.00 eligible for reward
+ assertEquals(MonetaryAmount.valueOf("8.00"), benefit);
+ }
+
+ @Test
+ public void testNoBenefitAvailable() {
+ // configure stub that always returns false
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(false));
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert zero eligible for reward
+ assertEquals(MonetaryAmount.valueOf("0.00"), benefit);
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy containing a single flag used to determine if benefit is available.
+ * Only useful for testing--a real availability policy might consider many factors such as the day of week of the
+ * dining, or the account's reward history for the current month.
+ */
+ private static class StubBenefitAvailibilityPolicy implements BenefitAvailabilityPolicy {
+
+ private final boolean isBenefitAvailable;
+
+ public StubBenefitAvailibilityPolicy(boolean isBenefitAvailable) {
+ this.isBenefitAvailable = isBenefitAvailable;
+ }
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return isBenefitAvailable;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/reward/AbstractRewardRepositoryTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/reward/AbstractRewardRepositoryTests.java
new file mode 100644
index 0000000..a9229e4
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/reward/AbstractRewardRepositoryTests.java
@@ -0,0 +1,94 @@
+package rewards.internal.reward;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.datasource.DataSourceUtils;
+import org.springframework.transaction.annotation.Transactional;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.Account;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+/**
+ * Tests the JDBC reward repository with a test data source to verify data
+ * access and relational-to-object mapping behavior works as expected.
+ */
+public abstract class AbstractRewardRepositoryTests {
+
+ @Autowired
+ protected JdbcRewardRepository rewardRepository;
+
+ @Autowired
+ protected DataSource dataSource;
+
+ @Test
+ public abstract void testProfile();
+
+ @Test
+ @Transactional
+ public void createReward() throws SQLException {
+ Dining dining = Dining.createDining("100.00", "1234123412341234",
+ "0123456789");
+
+ Account account = new Account("1", "Keith and Keri Donald");
+ account.setEntityId(0L);
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+
+ AccountContribution contribution = account
+ .makeContribution(MonetaryAmount.valueOf("8.00"));
+ RewardConfirmation confirmation = rewardRepository.confirmReward(
+ contribution, dining);
+ assertNotNull(confirmation, "confirmation should not be null");
+ assertNotNull("confirmation number should not be null",
+ confirmation.getConfirmationNumber());
+ assertEquals(contribution,
+ confirmation.getAccountContribution(), "wrong contribution object");
+ verifyRewardInserted(confirmation, dining);
+ }
+
+ private void verifyRewardInserted(RewardConfirmation confirmation,
+ Dining dining) throws SQLException {
+ assertEquals(1, getRewardCount());
+ Statement stmt = getCurrentConnection().createStatement();
+ ResultSet rs = stmt
+ .executeQuery("select REWARD_AMOUNT from T_REWARD where CONFIRMATION_NUMBER = '"
+ + confirmation.getConfirmationNumber() + "'");
+ rs.next();
+ assertEquals(confirmation.getAccountContribution().getAmount(),
+ MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private int getRewardCount() throws SQLException {
+ Statement stmt = getCurrentConnection().createStatement();
+ ResultSet rs = stmt.executeQuery("select count(*) from T_REWARD");
+ rs.next();
+ return rs.getInt(1);
+ }
+
+ /**
+ * Gets the connection behind the current transaction - this allows the
+ * tests to use the transaction created for the @Transactional test and see
+ * the changes made.
+ *
+ * Using a different (new) connection would fail because the T_REWARD table
+ * is locked by any updates and the queries in verifyRewardInserted
+ * and getRewardCount can never return.
+ *
+ * @return The current connection
+ */
+ private Connection getCurrentConnection() {
+ return DataSourceUtils.getConnection(dataSource);
+ }
+}
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/reward/JdbcRewardRepositoryIntegrationTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/reward/JdbcRewardRepositoryIntegrationTests.java
new file mode 100644
index 0000000..3d2576d
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/reward/JdbcRewardRepositoryIntegrationTests.java
@@ -0,0 +1,29 @@
+package rewards.internal.reward;
+
+
+import config.AppConfig;
+import config.DbConfig;
+import org.junit.jupiter.api.Test;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+
+/**
+ * Integration test for the JDBC-based rewards repository implementation.
+ * Verifies that the JdbcRewardRepository works with its underlying components
+ * and that Spring is configuring things properly.
+ */
+@ActiveProfiles("jpa")
+@SpringJUnitConfig(classes = {AppConfig.class, DbConfig.class})
+public class JdbcRewardRepositoryIntegrationTests extends
+ AbstractRewardRepositoryTests {
+
+ @Test
+ @Override
+ public void testProfile() {
+ assertEquals(JdbcRewardRepository.TYPE, rewardRepository.getInfo(), "JDBC expected but found " + rewardRepository.getInfo());
+ }
+
+}
diff --git a/lab/01-rewards-db/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java b/lab/01-rewards-db/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
new file mode 100644
index 0000000..3c79091
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
@@ -0,0 +1,35 @@
+package rewards.internal.reward;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests the JDBC reward repository with a test data source to test repository
+ * behavior and verifies the Reward JDBC code is correct.
+ */
+public class JdbcRewardRepositoryTests extends AbstractRewardRepositoryTests {
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ rewardRepository = new JdbcRewardRepository(dataSource);
+ }
+
+ @Test
+ @Override
+ public void testProfile() {
+ assertTrue(
+ rewardRepository instanceof JdbcRewardRepository, "JDBC expected");
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder().setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql").build();
+ }
+}
diff --git a/lab/01-rewards-db/src/test/java/utils/DataManagementSetup.java b/lab/01-rewards-db/src/test/java/utils/DataManagementSetup.java
new file mode 100644
index 0000000..4f3621a
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/utils/DataManagementSetup.java
@@ -0,0 +1,101 @@
+package utils;
+
+import java.util.Properties;
+
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.EntityManagerFactory;
+import javax.sql.DataSource;
+
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.orm.jpa.JpaTransactionManager;
+import org.springframework.orm.jpa.JpaVendorAdapter;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.Database;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.transaction.PlatformTransactionManager;
+
+/**
+ * Setup a JPA data-management layer without using Spring (for testing).
+ */
+public class DataManagementSetup {
+
+ public static final String DOMAIN_OBJECTS_PARENT_PACKAGE = "rewards.internal";
+
+ private DataSource dataSource;
+ private EntityManagerFactory entityManagerFactory;
+ private PlatformTransactionManager transactionManager;
+
+ public DataManagementSetup() {
+ }
+
+ private void setup() {
+ if (dataSource == null) {
+ dataSource = createTestDataSource();
+ entityManagerFactory = createEntityManagerFactory();
+ transactionManager = createTransactionManager();
+ }
+ }
+
+ public DataSource getDataSource() {
+ setup();
+ return dataSource;
+ }
+
+ public EntityManager createEntityManager() {
+ setup();
+ return entityManagerFactory.createEntityManager();
+ }
+
+ public PlatformTransactionManager getTransactionManager() {
+ setup();
+ return transactionManager;
+ }
+
+ // - - - - - - - - - - - - - - - INTERNAL METHODS - - - - - - - - - - - - - - -
+
+ protected DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder().setName("rewards") //
+ .addScript("/rewards/testdb/schema.sql") //
+ .addScript("/rewards/testdb/data.sql") //
+ .build();
+ }
+
+ protected JpaTransactionManager createTransactionManager() {
+ return new JpaTransactionManager(entityManagerFactory);
+ }
+
+ protected JpaVendorAdapter createVendorAdapter() {
+ HibernateJpaVendorAdapter adapter = new HibernateJpaVendorAdapter();
+ adapter.setDatabase(Database.HSQL);
+ return adapter;
+ }
+
+ protected Properties createJpaProperties() {
+ Properties properties = new Properties();
+ // turn on formatted SQL logging (very useful to verify Jpa is
+ // issuing proper SQL)
+ properties.setProperty("hibenate.show_sql", "true");
+ properties.setProperty("hibernate.format_sql", "true");
+ return properties;
+ }
+
+ protected final EntityManagerFactory createEntityManagerFactory() {
+
+ // Create a FactoryBean to help create a JPA EntityManagerFactory
+ LocalContainerEntityManagerFactoryBean factoryBean = new LocalContainerEntityManagerFactoryBean();
+ factoryBean.setDataSource(dataSource);
+ factoryBean.setJpaVendorAdapter(createVendorAdapter());
+ factoryBean.setJpaProperties(createJpaProperties());
+
+ // Not using persistence unit or persistence.xml, so need to tell
+ // JPA where to find Entities
+ factoryBean.setPackagesToScan(DOMAIN_OBJECTS_PARENT_PACKAGE);
+
+ // initialize according to the Spring InitializingBean contract
+ factoryBean.afterPropertiesSet();
+
+ // get the created session factory
+ return factoryBean.getObject();
+ }
+
+}
diff --git a/lab/01-rewards-db/src/test/java/utils/TransactionUtils.java b/lab/01-rewards-db/src/test/java/utils/TransactionUtils.java
new file mode 100644
index 0000000..f49dcbc
--- /dev/null
+++ b/lab/01-rewards-db/src/test/java/utils/TransactionUtils.java
@@ -0,0 +1,145 @@
+package utils;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import ch.qos.logback.classic.Level;
+
+import org.springframework.transaction.IllegalTransactionStateException;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+
+/**
+ * Utility class for managing Transactions without Spring (in tests).
+ *
+ * Note: With Spring's @Transactional tests, you don't need any of this
+ * unless you need to run multiple transactions in a single test.
+ */
+public class TransactionUtils {
+
+ protected final PlatformTransactionManager transactionManager;
+ protected final Logger logger;
+
+ private TransactionStatus transactionStatus;
+
+ /**
+ * Create an instance using any available transaction manager.
+ *
+ * @param transactionManager
+ * In our tests, either a DataSourceTransactionManager or a
+ * JpaTransactionManager.
+ */
+ public TransactionUtils(PlatformTransactionManager transactionManager) {
+ assert (transactionManager != null);
+
+ this.transactionManager = transactionManager;
+
+ logger = LoggerFactory.getLogger(getClass());
+ if (logger instanceof ch.qos.logback.classic.Logger logger1)
+ logger1.setLevel(Level.INFO);
+
+ }
+
+ /**
+ * Begin a new transaction, ensuring one is not running already
+ *
+ */
+ public void beginTransaction() {
+ // Make sure no transaction is running
+ try {
+ transactionStatus = transactionManager
+ .getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_MANDATORY));
+ assert (false); // Force an exception - there should be NO transaction
+ } catch (IllegalTransactionStateException e) {
+ // Expected behavior, continue
+ }
+
+ // Begin a new transaction - just checked that there isn't one
+ transactionStatus = transactionManager
+ .getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_REQUIRED));
+
+ assert (transactionStatus != null);
+ assert (transactionStatus.isNewTransaction());
+ logger.info("NEW " + transactionStatus + " - completed = " + transactionStatus.isCompleted());
+ }
+
+ /**
+ * Rollback the current transaction - there must be one.
+ *
+ */
+ public void rollbackTransaction() {
+ // Make sure an exception is running
+ try {
+ transactionManager
+ .getTransaction(new DefaultTransactionDefinition(TransactionDefinition.PROPAGATION_MANDATORY));
+ // Expected behavior, continue
+ } catch (IllegalTransactionStateException e) {
+ assert (false); // Force an exception - there should be a transaction
+ }
+
+ // Rollback the transaction to avoid corrupting other tests
+ logger.info("ROLLBACKK " + transactionStatus);
+ transactionManager.rollback(transactionStatus);
+ }
+
+ /**
+ * Get the current transaction - there should be one.
+ *
+ * @return
+ */
+ public TransactionStatus getCurrentTransaction() {
+ TransactionDefinition definition = new DefaultTransactionDefinition(
+ DefaultTransactionDefinition.PROPAGATION_MANDATORY);
+ TransactionStatus transaction = transactionManager.getTransaction(definition);
+ logger.info("TRANSACTION = " + transaction);
+ return transaction;
+ }
+
+ /**
+ * Get a transaction if there is one or start a new one otherwise.
+ *
+ * @return The transaction.
+ */
+ public TransactionStatus getTransaction() {
+ TransactionDefinition definition = new DefaultTransactionDefinition(
+ DefaultTransactionDefinition.PROPAGATION_REQUIRED);
+ TransactionStatus transaction = transactionManager.getTransaction(definition);
+ logger.info("TRANSACTION = " + transaction);
+ return transaction;
+ }
+
+ /**
+ * Start a brand new transaction - forcing a new one if one exists already
+ * (using {@link TransactionDefinition#PROPAGATION_REQUIRES_NEW}).
+ *
+ * @return
+ */
+ public TransactionStatus getNewTransaction() {
+ TransactionDefinition definition = new DefaultTransactionDefinition(
+ DefaultTransactionDefinition.PROPAGATION_REQUIRES_NEW);
+ TransactionStatus transaction = transactionManager.getTransaction(definition);
+ logger.info("TRANSACTION = " + transaction);
+ return transaction;
+ }
+
+ /**
+ * Is there a transaction running already?
+ *
+ * @return Yes or no.
+ */
+ public boolean transactionExists() {
+ try {
+ TransactionStatus transaction = getCurrentTransaction();
+
+ if (transaction == null)
+ throw new IllegalStateException("No transaction in progress");
+
+ logger.info("TRANSACTION EXISTS - new ? " + transaction.isNewTransaction());
+ return true;
+ } catch (Exception e) {
+ logger.error("NO TRANSACTION: " + e);
+ return false;
+ }
+ }
+}
diff --git a/lab/10-spring-intro-solution/build.gradle b/lab/10-spring-intro-solution/build.gradle
new file mode 100644
index 0000000..af9f0d4
--- /dev/null
+++ b/lab/10-spring-intro-solution/build.gradle
@@ -0,0 +1,3 @@
+dependencies {
+ implementation project(':00-rewards-common')
+}
diff --git a/lab/10-spring-intro-solution/pom.xml b/lab/10-spring-intro-solution/pom.xml
new file mode 100644
index 0000000..94a8496
--- /dev/null
+++ b/lab/10-spring-intro-solution/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+ 10-spring-intro-solution
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/AccountContribution.java b/lab/10-spring-intro-solution/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/Dining.java b/lab/10-spring-intro-solution/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/RewardConfirmation.java b/lab/10-spring-intro-solution/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/RewardNetwork.java b/lab/10-spring-intro-solution/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/10-spring-intro-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..cb3191e
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,52 @@
+package rewards.internal;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/Account.java b/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..7b48fa3
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,141 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account.
+ *
+ * Callers should not attempt to hold on or modify the returned set. This method should only be used transitively;
+ * for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/AccountRepository.java b/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/Beneficiary.java b/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/package.html b/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/10-spring-intro-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..aa642ae
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,79 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on a benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/10-spring-intro-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/internal/restaurant/package.html b/lab/10-spring-intro-solution/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/10-spring-intro-solution/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..1207f0f
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/main/java/rewards/internal/reward/package.html b/lab/10-spring-intro-solution/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/10-spring-intro-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/10-spring-intro-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..86e3e42
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,72 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
diff --git a/lab/10-spring-intro-solution/src/test/java/rewards/internal/StubAccountRepository.java b/lab/10-spring-intro-solution/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..625fee6
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,41 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new RuntimeException("no account has been found for credit card number " + creditCardNumber);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/10-spring-intro-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..b812e9e
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,36 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new RuntimeException("no restaurant has been found for merchant number " + merchantNumber);
+ }
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro-solution/src/test/java/rewards/internal/StubRewardRepository.java b/lab/10-spring-intro-solution/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..2487aca
--- /dev/null
+++ b/lab/10-spring-intro-solution/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/build.gradle b/lab/10-spring-intro/build.gradle
new file mode 100644
index 0000000..af9f0d4
--- /dev/null
+++ b/lab/10-spring-intro/build.gradle
@@ -0,0 +1,3 @@
+dependencies {
+ implementation project(':00-rewards-common')
+}
diff --git a/lab/10-spring-intro/pom.xml b/lab/10-spring-intro/pom.xml
new file mode 100644
index 0000000..31f45c5
--- /dev/null
+++ b/lab/10-spring-intro/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+ 10-spring-intro
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
diff --git a/lab/10-spring-intro/src/main/java/rewards/AccountContribution.java b/lab/10-spring-intro/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..4638d42
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private final String accountNumber;
+
+ private final MonetaryAmount amount;
+
+ private final Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private final String beneficiary;
+
+ private final MonetaryAmount amount;
+
+ private final Percentage percentage;
+
+ private final MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/main/java/rewards/Dining.java b/lab/10-spring-intro/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..b7d4609
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private final MonetaryAmount amount;
+
+ private final String creditCardNumber;
+
+ private final String merchantNumber;
+
+ private final SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a restaurant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a restaurant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a restaurant on the date
+ * specified. A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/main/java/rewards/RewardConfirmation.java b/lab/10-spring-intro/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..1a55bbd
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private final String confirmationNumber;
+
+ private final AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/main/java/rewards/RewardNetwork.java b/lab/10-spring-intro/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..1d67583
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/10-spring-intro/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..df35e2f
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,58 @@
+package rewards.internal;
+
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ *
+ * TODO-00: In this lab, you are going to exercise the following:
+ * - Understanding internal operations that need to be performed to implement
+ * "rewardAccountFor" method of the "RewardNetworkImpl" class
+ * - Writing test code using stub implementations of dependencies
+ * - Writing both target code and test code without using Spring framework
+ *
+ * TODO-01: Review the Rewards Application document (Refer to the lab document)
+ * TODO-02: Review project dependencies (Refer to the lab document)
+ * TODO-03: Review Rewards Commons project (Refer to the lab document)
+ * TODO-04: Review RewardNetwork interface and RewardNetworkImpl class below
+ * TODO-05: Review the RewardNetworkImpl configuration logic (Refer to the lab document)
+ * TODO-06: Review sequence diagram (Refer to the lab document)
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ // TODO-07: Write code here for rewarding an account according to
+ // the sequence diagram in the lab document
+ // TODO-08: Return the corresponding reward confirmation
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/main/java/rewards/internal/account/Account.java b/lab/10-spring-intro/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..c5e9ed8
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,141 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private final Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account.
+ *
+ * Callers should not attempt to hold on or modify the returned set. This method should only be used transitively;
+ * for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/main/java/rewards/internal/account/AccountRepository.java b/lab/10-spring-intro/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..7d0ad44
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/main/java/rewards/internal/account/Beneficiary.java b/lab/10-spring-intro/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/main/java/rewards/internal/account/package.html b/lab/10-spring-intro/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/lab/10-spring-intro/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/10-spring-intro/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..aa642ae
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,79 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on a benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/10-spring-intro/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..c74f02a
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/10-spring-intro/src/main/java/rewards/internal/restaurant/package.html b/lab/10-spring-intro/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/10-spring-intro/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/10-spring-intro/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..cbcc55d
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/main/java/rewards/internal/reward/package.html b/lab/10-spring-intro/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/10-spring-intro/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/10-spring-intro/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/10-spring-intro/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..fa09350
--- /dev/null
+++ b/lab/10-spring-intro/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,81 @@
+package rewards.internal;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic.
+ * Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without
+ * the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way
+ * for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they
+ * are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ // TODO-09: Review the test setup
+ @BeforeEach
+ public void setUp() {
+ // Create stubs to facilitate fast in-memory testing with
+ // dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // Setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ // TODO-10: Test RewardNetworkImpl class
+ // - Remove the @Disabled annotation below.
+ // - Run this JUnit test. Verify it passes.
+ @Test
+ @Disabled
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/test/java/rewards/internal/StubAccountRepository.java b/lab/10-spring-intro/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..625fee6
--- /dev/null
+++ b/lab/10-spring-intro/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,41 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new RuntimeException("no account has been found for credit card number " + creditCardNumber);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/10-spring-intro/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..c7392ea
--- /dev/null
+++ b/lab/10-spring-intro/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,36 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new RuntimeException("no restaurant has been found for merchant number " + merchantNumber);
+ }
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/10-spring-intro/src/test/java/rewards/internal/StubRewardRepository.java b/lab/10-spring-intro/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..2487aca
--- /dev/null
+++ b/lab/10-spring-intro/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/build.gradle b/lab/12-javaconfig-dependency-injection-solution/build.gradle
new file mode 100644
index 0000000..4d6c184
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/build.gradle
@@ -0,0 +1,7 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ */
+
+dependencies {
+ implementation project(':00-rewards-common')
+}
diff --git a/lab/12-javaconfig-dependency-injection-solution/pom.xml b/lab/12-javaconfig-dependency-injection-solution/pom.xml
new file mode 100644
index 0000000..daea2ac
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+ 12-javaconfig-dependency-injection-solution
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/config/RewardsConfig.java b/lab/12-javaconfig-dependency-injection-solution/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..49d9626
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,55 @@
+package config;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import rewards.RewardNetwork;
+import rewards.internal.RewardNetworkImpl;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.account.JdbcAccountRepository;
+import rewards.internal.restaurant.JdbcRestaurantRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.JdbcRewardRepository;
+import rewards.internal.reward.RewardRepository;
+
+@Configuration
+public class RewardsConfig {
+ private final DataSource dataSource;
+
+ // As this is the only constructor, @Autowired is not needed.
+ public RewardsConfig(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ @Bean
+ public RewardNetwork rewardNetwork(){
+ return new RewardNetworkImpl(
+ accountRepository(),
+ restaurantRepository(),
+ rewardRepository());
+ }
+
+ @Bean
+ public AccountRepository accountRepository(){
+ JdbcAccountRepository repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RestaurantRepository restaurantRepository(){
+ JdbcRestaurantRepository repository = new JdbcRestaurantRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RewardRepository rewardRepository(){
+ JdbcRewardRepository repository = new JdbcRewardRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+}
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/AccountContribution.java b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/Dining.java b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/RewardConfirmation.java b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/RewardNetwork.java b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..cb3191e
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,52 @@
+package rewards.internal;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/Account.java b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..7b48fa3
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,141 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account.
+ *
+ * Callers should not attempt to hold on or modify the returned set. This method should only be used transitively;
+ * for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/AccountRepository.java b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..ec0fdde
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,30 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ *
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/Beneficiary.java b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..5181837
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,139 @@
+package rewards.internal.account;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+public class JdbcAccountRepository implements AccountRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to load accounts.
+ * @param dataSource the data source
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+
+ String sql = """
+ select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, \
+ b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS \
+ from T_ACCOUNT a, T_ACCOUNT_CREDIT_CARD c \
+ left outer join T_ACCOUNT_BENEFICIARY b \
+ on a.ID = b.ACCOUNT_ID \
+ where c.ACCOUNT_ID = a.ID and c.NUMBER = ?\
+ """;
+
+ Account account = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, creditCardNumber);
+ rs = ps.executeQuery();
+ account = mapAccount(rs);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred finding by credit card number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ for (Beneficiary beneficiary : account.getBeneficiaries()) {
+ ps.setBigDecimal(1, beneficiary.getSavings().asBigDecimal());
+ ps.setLong(2, account.getEntityId());
+ ps.setString(3, beneficiary.getName());
+ ps.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred updating beneficiary savings", e);
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ Beneficiary b = mapBeneficiary(rs);
+ if (b != null) {
+ account.restoreBeneficiary(b);
+ }
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ if (name == null) {
+ // apparently no beneficiary for this
+ return null;
+ }
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/package.html b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/test/java/config/RewardsConfigTests.java b/lab/12-javaconfig-dependency-injection-solution/src/test/java/config/RewardsConfigTests.java
new file mode 100644
index 0000000..69f6ad8
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/test/java/config/RewardsConfigTests.java
@@ -0,0 +1,71 @@
+package config;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.lang.reflect.Field;
+
+import javax.sql.DataSource;
+
+import org.assertj.core.api.Fail;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import rewards.RewardNetwork;
+import rewards.internal.RewardNetworkImpl;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.account.JdbcAccountRepository;
+import rewards.internal.restaurant.JdbcRestaurantRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.JdbcRewardRepository;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * Unit test the Spring configuration class to ensure it is creating the right
+ * beans.
+ */
+public class RewardsConfigTests {
+ // Provide a mock for testing
+ private final DataSource dataSource = Mockito.mock(DataSource.class);
+
+ private final RewardsConfig rewardsConfig = new RewardsConfig(dataSource);
+
+ @Test
+ public void getBeans() {
+ RewardNetwork rewardNetwork = rewardsConfig.rewardNetwork();
+ assertTrue(rewardNetwork instanceof RewardNetworkImpl);
+
+ AccountRepository accountRepository = rewardsConfig.accountRepository();
+ assertTrue(accountRepository instanceof JdbcAccountRepository);
+ checkDataSource(accountRepository);
+
+ RestaurantRepository restaurantRepository = rewardsConfig.restaurantRepository();
+ assertTrue(restaurantRepository instanceof JdbcRestaurantRepository);
+ checkDataSource(restaurantRepository);
+
+ RewardRepository rewardsRepository = rewardsConfig.rewardRepository();
+ assertTrue(rewardsRepository instanceof JdbcRewardRepository);
+ checkDataSource(rewardsRepository);
+ }
+
+ /**
+ * Ensure the data-source is set for the repository. Uses reflection as we do
+ * not wish to provide a getDataSource() method.
+ *
+ * @param repository
+ */
+ private void checkDataSource(Object repository) {
+ Class> repositoryClass = repository.getClass();
+
+ try {
+ Field dataSource = repositoryClass.getDeclaredField("dataSource");
+ dataSource.setAccessible(true);
+ assertNotNull(dataSource.get(repository));
+ } catch (Exception e) {
+ String failureMessage = "Unable to validate dataSource in " + repositoryClass.getSimpleName();
+ System.out.println(failureMessage);
+ e.printStackTrace();
+ Fail.fail(failureMessage);
+ }
+ }
+}
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/RewardNetworkTests.java b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..fd10077
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,65 @@
+package rewards;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ApplicationContext;
+
+import common.money.MonetaryAmount;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application work together to reward for dining
+ * successfully. Uses Spring to bootstrap the application for use in a test environment.
+ */
+public class RewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetwork rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // Create the test configuration for the application:
+
+ ApplicationContext context = SpringApplication.run(TestInfrastructureConfig.class);
+
+ // Get the bean to use to invoke the application
+ rewardNetwork = context.getBean(RewardNetwork.class);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ // this fails if you have selected an account without beneficiaries!
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/TestInfrastructureConfig.java b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/TestInfrastructureConfig.java
new file mode 100644
index 0000000..bd29e01
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/TestInfrastructureConfig.java
@@ -0,0 +1,28 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import config.RewardsConfig;
+
+@Configuration
+@Import(RewardsConfig.class)
+public class TestInfrastructureConfig {
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..98b7353
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,72 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/StubAccountRepository.java b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..b926be2
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,43 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..ce1f820
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,38 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/StubRewardRepository.java b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..2487aca
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection-solution/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/build.gradle b/lab/12-javaconfig-dependency-injection/build.gradle
new file mode 100644
index 0000000..4d6c184
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/build.gradle
@@ -0,0 +1,7 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ */
+
+dependencies {
+ implementation project(':00-rewards-common')
+}
diff --git a/lab/12-javaconfig-dependency-injection/pom.xml b/lab/12-javaconfig-dependency-injection/pom.xml
new file mode 100644
index 0000000..b0e372c
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+ 12-javaconfig-dependency-injection
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/config/RewardsConfig.java b/lab/12-javaconfig-dependency-injection/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..4d72577
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,50 @@
+package config;
+
+import javax.sql.DataSource;
+
+/**
+ * TODO-00: In this lab, you are going to exercise the following:
+ * - Creating Spring configuration class
+ * - Defining bean definitions within the configuration class
+ * - Specifying the dependency relationships among beans
+ * - Injecting dependencies through constructor injection
+ * - Creating Spring application context in the test code
+ * (WITHOUT using Spring testContext framework)
+ *
+ * TODO-01: Make this class a Spring configuration class
+ * - Use an appropriate annotation.
+ *
+ * TODO-02: Define four empty @Bean methods, one for the
+ * reward-network and three for the repositories.
+ * - The names of the beans should be:
+ * - rewardNetwork
+ * - accountRepository
+ * - restaurantRepository
+ * - rewardRepository
+ *
+ * TODO-03: Inject DataSource through constructor injection
+ * - Each repository implementation has a DataSource
+ * property to be set, but the DataSource is defined
+ * elsewhere (TestInfrastructureConfig.java), so you
+ * will need to define a constructor for this class
+ * that accepts a DataSource parameter.
+ * - As it is the only constructor, @Autowired is optional.
+ *
+ * TODO-04: Implement each @Bean method to contain the code
+ * needed to instantiate its object and set its
+ * dependencies
+ * - You can create beans from the following implementation classes
+ * - rewardNetwork bean from RewardNetworkImpl class
+ * - accountRepository bean from JdbcAccountRepository class
+ * - restaurantRepository bean from JdbcRestaurantRepository class
+ * - rewardRepository bean from JdbcRewardRepository class
+ * - Note that return type of each bean method should be an interface
+ * not an implementation.
+ */
+
+public class RewardsConfig {
+
+ // Set this by adding a constructor.
+ private DataSource dataSource;
+
+}
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/config/package.html b/lab/12-javaconfig-dependency-injection/src/main/java/config/package.html
new file mode 100644
index 0000000..a2ab399
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/config/package.html
@@ -0,0 +1,7 @@
+
+
+
+Configuration classes for the RewardNetwork.
+
+
+
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/rewards/AccountContribution.java b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/rewards/Dining.java b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..4bc4907
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a restaurant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a restaurant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a restaurant on the date
+ * specified. A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/rewards/RewardConfirmation.java b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/rewards/RewardNetwork.java b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..cb3191e
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,52 @@
+package rewards.internal;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/Account.java b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..7b48fa3
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,141 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account.
+ *
+ * Callers should not attempt to hold on or modify the returned set. This method should only be used transitively;
+ * for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/AccountRepository.java b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/Beneficiary.java b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..5181837
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,139 @@
+package rewards.internal.account;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+public class JdbcAccountRepository implements AccountRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to load accounts.
+ * @param dataSource the data source
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+
+ String sql = """
+ select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, \
+ b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS \
+ from T_ACCOUNT a, T_ACCOUNT_CREDIT_CARD c \
+ left outer join T_ACCOUNT_BENEFICIARY b \
+ on a.ID = b.ACCOUNT_ID \
+ where c.ACCOUNT_ID = a.ID and c.NUMBER = ?\
+ """;
+
+ Account account = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, creditCardNumber);
+ rs = ps.executeQuery();
+ account = mapAccount(rs);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred finding by credit card number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ for (Beneficiary beneficiary : account.getBeneficiaries()) {
+ ps.setBigDecimal(1, beneficiary.getSavings().asBigDecimal());
+ ps.setLong(2, account.getEntityId());
+ ps.setString(3, beneficiary.getName());
+ ps.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred updating beneficiary savings", e);
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ Beneficiary b = mapBeneficiary(rs);
+ if (b != null) {
+ account.restoreBeneficiary(b);
+ }
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ if (name == null) {
+ // apparently no beneficiary for this
+ return null;
+ }
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/package.html b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/12-javaconfig-dependency-injection/src/test/java/config/RewardsConfigTests.java b/lab/12-javaconfig-dependency-injection/src/test/java/config/RewardsConfigTests.java
new file mode 100644
index 0000000..31169d3
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/test/java/config/RewardsConfigTests.java
@@ -0,0 +1,69 @@
+package config;
+
+import org.assertj.core.api.Fail;
+import org.mockito.Mockito;
+
+import javax.sql.DataSource;
+import java.lang.reflect.Field;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Unit test the Spring configuration class to ensure it is creating the right
+ * beans.
+ */
+@SuppressWarnings("unused")
+public class RewardsConfigTests {
+ // Provide a mock object for testing
+ private final DataSource dataSource = Mockito.mock(DataSource.class);
+
+ // TODO-05: Run the test
+ // - Uncomment the code below between /* and */
+ // - If you have implemented RewardsConfig as requested it should compile.
+ // - Fix RewardsConfig if necessary.
+ // - Now run the test, it should pass.
+
+ /*
+ private RewardsConfig rewardsConfig = new RewardsConfig(dataSource);
+
+ @Test
+ public void getBeans() {
+ RewardNetwork rewardNetwork = rewardsConfig.rewardNetwork();
+ assertTrue(rewardNetwork instanceof RewardNetworkImpl);
+
+ AccountRepository accountRepository = rewardsConfig.accountRepository();
+ assertTrue(accountRepository instanceof JdbcAccountRepository);
+ checkDataSource(accountRepository);
+
+ RestaurantRepository restaurantRepository = rewardsConfig.restaurantRepository();
+ assertTrue(restaurantRepository instanceof JdbcRestaurantRepository);
+ checkDataSource(restaurantRepository);
+
+ RewardRepository rewardsRepository = rewardsConfig.rewardRepository();
+ assertTrue(rewardsRepository instanceof JdbcRewardRepository);
+ checkDataSource(rewardsRepository);
+ }
+ */
+
+ /**
+ * Ensure the data-source is set for the repository. Uses reflection as we do
+ * not wish to provide a getDataSource() method.
+ *
+ * @param repository One of our three repositories.
+ *
+ */
+ private void checkDataSource(Object repository) {
+ Class> repositoryClass = repository.getClass();
+
+ try {
+ Field dataSource = repositoryClass.getDeclaredField("dataSource");
+ dataSource.setAccessible(true);
+ assertNotNull(dataSource.get(repository));
+ } catch (Exception e) {
+ String failureMessage = "Unable to validate dataSource in " + repositoryClass.getSimpleName();
+ System.out.println(failureMessage);
+ e.printStackTrace();
+ Fail.fail(failureMessage);
+ }
+ }
+}
diff --git a/lab/12-javaconfig-dependency-injection/src/test/java/rewards/TestInfrastructureConfig.java b/lab/12-javaconfig-dependency-injection/src/test/java/rewards/TestInfrastructureConfig.java
new file mode 100644
index 0000000..90f3b4b
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/test/java/rewards/TestInfrastructureConfig.java
@@ -0,0 +1,69 @@
+package rewards;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+/**
+ * TODO-06: Study this configuration class used for testing
+ * - It contains a @Bean method that returns DataSource.
+ * - It also creates and populates in-memory HSQL database tables
+ * using two SQL scripts.
+ * - Note that the two scripts are located under the
+ * 'src/main/resources/rewards/testdb' directory of
+ * the '00-rewards-common' project
+ * - Do not modify this method.
+ *
+ * TODO-07: Import your application configuration file (RewardsConfig)
+ * - Now the test code should have access to all the beans defined in
+ * the RewardsConfig configuration class
+ *
+ * TODO-08: Create a new JUnit 5 test class
+ * - Call it RewardNetworkTests
+ * - Create it in the same package this configuration class is located.
+ * - Ask for a setUp() method to be generated within your IDE.
+ *
+ * NOTE: The appendices at the bottom of the course Home Page includes
+ * a section on creating JUnit tests in an IDE.
+ *
+ * TODO-09: Make sure the setUp() method in the RewardNetworkTests class is annotated with @BeforeEach.
+ * - In the setUp() method, create an application context using
+ * this configuration class - use run(..) static method of
+ * the SpringApplication class
+ * - Then get the 'rewardNetwork' bean from the application context
+ * and assign it to a private field for use later.
+ *
+ * TODO-10: We can test the setup by running an empty test.
+ * - If your IDE automatically generated a @Test method, rename it
+ * testRewardForDining. Delete any code in the method body.
+ * - Otherwise add a testRewardForDining method & annotate it with
+ * @Test (make sure the @Test is from org.junit.jupiter.api.Test ).
+ * - Run the test. If your setup() is working, you get a green bar.
+ *
+ * TODO-11: Finally run a real test.
+ * - Copy the unit test (the @Test method) from
+ * RewardNetworkImplTests#testRewardForDining() under
+ * rewards.internal test package - we are testing
+ * the same code, but using a different setup.
+ * - Run the test - it should pass if you have configured everything
+ * correctly. Congratulations, you are done.
+ * - If your test fails - did you miss the import in TO DO 7 above?
+ *
+ */
+@Configuration
+public class TestInfrastructureConfig {
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource() {
+ return (new EmbeddedDatabaseBuilder()) //
+ .addScript("classpath:rewards/testdb/schema.sql") //
+ .addScript("classpath:rewards/testdb/data.sql") //
+ .build();
+ }
+}
diff --git a/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..507e154
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,73 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/StubAccountRepository.java b/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..b926be2
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,43 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..ce1f820
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,38 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/StubRewardRepository.java b/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..2487aca
--- /dev/null
+++ b/lab/12-javaconfig-dependency-injection/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/12-javaconfig-dependency-injection/src/test/resources/scripts-in-rewards-common.txt b/lab/12-javaconfig-dependency-injection/src/test/resources/scripts-in-rewards-common.txt
new file mode 100644
index 0000000..e69de29
diff --git a/lab/16-annotations-solution/build.gradle b/lab/16-annotations-solution/build.gradle
new file mode 100644
index 0000000..4d6c184
--- /dev/null
+++ b/lab/16-annotations-solution/build.gradle
@@ -0,0 +1,7 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ */
+
+dependencies {
+ implementation project(':00-rewards-common')
+}
diff --git a/lab/16-annotations-solution/pom.xml b/lab/16-annotations-solution/pom.xml
new file mode 100644
index 0000000..a0b50e6
--- /dev/null
+++ b/lab/16-annotations-solution/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+ 16-annotations-solution
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
diff --git a/lab/16-annotations-solution/src/main/java/config/RewardsConfig.java b/lab/16-annotations-solution/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..c51dd3a
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,10 @@
+package config;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ComponentScan("rewards.internal")
+public class RewardsConfig {
+
+}
diff --git a/lab/16-annotations-solution/src/main/java/rewards/AccountContribution.java b/lab/16-annotations-solution/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/Dining.java b/lab/16-annotations-solution/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/RewardConfirmation.java b/lab/16-annotations-solution/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/RewardNetwork.java b/lab/16-annotations-solution/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/16-annotations-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..d07b40a
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,55 @@
+package rewards.internal;
+
+import org.springframework.stereotype.Service;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+@Service("rewardNetwork")
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/account/Account.java b/lab/16-annotations-solution/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..7b48fa3
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,141 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account.
+ *
+ * Callers should not attempt to hold on or modify the returned set. This method should only be used transitively;
+ * for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/account/AccountRepository.java b/lab/16-annotations-solution/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/account/Beneficiary.java b/lab/16-annotations-solution/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/16-annotations-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..e3b9821
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,129 @@
+package rewards.internal.account;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.stereotype.Repository;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+@Repository("accountRepository")
+public class JdbcAccountRepository implements AccountRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to load accounts.
+ * @param dataSource the data source
+ */
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = "select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?";
+ Account account = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, creditCardNumber);
+ rs = ps.executeQuery();
+ account = mapAccount(rs);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred finding by credit card number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ for (Beneficiary beneficiary : account.getBeneficiaries()) {
+ ps.setBigDecimal(1, beneficiary.getSavings().asBigDecimal());
+ ps.setLong(2, account.getEntityId());
+ ps.setString(3, beneficiary.getName());
+ ps.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred updating beneficiary savings", e);
+ }
+ // Close to prevent database cursor exhaustion
+ // Close to prevent database connection exhaustion
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/account/package.html b/lab/16-annotations-solution/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..254f6a8
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,118 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.stereotype.Repository;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ *
+ * This implementation caches restaurants to improve performance.
+ * The cache is populated on initialization and cleared on destruction.
+ */
+@Repository("restaurantRepository")
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * The Restaurant object cache. Cached restaurants are indexed by their merchant numbers.
+ */
+ private Map restaurantCache;
+
+ /**
+ * The constructor sets the data source this repository will use to load restaurants.
+ * When the instance of JdbcRestaurantRepository is created, a Restaurant cache is
+ * populated for read only access
+ *
+ * @param dataSource the data source
+ */
+
+ public JdbcRestaurantRepository(DataSource dataSource){
+ this.dataSource = dataSource;
+ this.populateRestaurantCache();
+ }
+
+ public JdbcRestaurantRepository(){}
+
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ return queryRestaurantCache(merchantNumber);
+ }
+
+ /**
+ * Helper method that populates the {@link #restaurantCache restaurant object cache} from rows in the T_RESTAURANT
+ * table. Cached restaurants are indexed by their merchant numbers. This method is called on initialization.
+ */
+ @PostConstruct
+ void populateRestaurantCache() {
+ restaurantCache = new HashMap<>();
+ String sql = "select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE from T_RESTAURANT";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ResultSet rs = ps.executeQuery()) {
+ while (rs.next()) {
+ Restaurant restaurant = mapRestaurant(rs);
+ // index the restaurant by its merchant number
+ restaurantCache.put(restaurant.getNumber(), restaurant);
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred finding by merchant number", e);
+ }
+ }
+
+ /**
+ * Helper method that simply queries the cache of restaurants.
+ *
+ * @param merchantNumber the restaurant's merchant number
+ * @return the restaurant
+ * @throws EmptyResultDataAccessException if no restaurant was found with that merchant number
+ */
+ private Restaurant queryRestaurantCache(String merchantNumber) {
+ Restaurant restaurant = restaurantCache.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+
+ /**
+ * Helper method that clears the cache of restaurants. This method is called on destruction
+ */
+ @PreDestroy
+ void clearRestaurantCache() {
+ restaurantCache.clear();
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+ // map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..aa642ae
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,79 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on a benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/package.html b/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/16-annotations-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..fddb461
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,61 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+import javax.sql.DataSource;
+import java.sql.*;
+
+/**
+ * JDBC implementation of a reward repository that records the result of a reward transaction by inserting a reward
+ * confirmation record.
+ */
+@Repository("rewardRepository")
+public class JdbcRewardRepository implements RewardRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to insert rewards.
+ * @param dataSource the data source
+ */
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ String confirmationNumber = nextConfirmationNumber();
+ ps.setString(1, confirmationNumber);
+ ps.setBigDecimal(2, contribution.getAmount().asBigDecimal());
+ ps.setDate(3, new Date(SimpleDate.today().inMilliseconds()));
+ ps.setString(4, contribution.getAccountNumber());
+ ps.setString(5, dining.getMerchantNumber());
+ ps.setDate(6, new Date(dining.getDate().inMilliseconds()));
+ ps.setBigDecimal(7, dining.getAmount().asBigDecimal());
+ ps.execute();
+ return new RewardConfirmation(confirmationNumber, contribution);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred inserting reward record", e);
+ }
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ResultSet rs = ps.executeQuery()) {
+ rs.next();
+ return rs.getString(1);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception getting next confirmation number", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/16-annotations-solution/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..1207f0f
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/main/java/rewards/internal/reward/package.html b/lab/16-annotations-solution/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/16-annotations-solution/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/16-annotations-solution/src/test/java/rewards/RewardNetworkTests.java b/lab/16-annotations-solution/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..8fa51ce
--- /dev/null
+++ b/lab/16-annotations-solution/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,67 @@
+package rewards;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ApplicationContext;
+
+import common.money.MonetaryAmount;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application work together to reward for dining
+ * successfully. Uses Spring to bootstrap the application for use in a test environment.
+ */
+public class RewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetwork rewardNetwork;
+
+
+ @BeforeEach
+ public void setUp() {
+ // Create the test configuration for the application from two classes:
+ ApplicationContext context = SpringApplication.run(TestInfrastructureConfig.class);
+
+ // Get the bean to use to invoke the application
+ rewardNetwork = context.getBean(RewardNetwork.class);
+
+ }
+
+
+
+ @Test
+ public void rewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
diff --git a/lab/16-annotations-solution/src/test/java/rewards/TestInfrastructureConfig.java b/lab/16-annotations-solution/src/test/java/rewards/TestInfrastructureConfig.java
new file mode 100644
index 0000000..bd29e01
--- /dev/null
+++ b/lab/16-annotations-solution/src/test/java/rewards/TestInfrastructureConfig.java
@@ -0,0 +1,28 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import config.RewardsConfig;
+
+@Configuration
+@Import(RewardsConfig.class)
+public class TestInfrastructureConfig {
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/16-annotations-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/16-annotations-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..98b7353
--- /dev/null
+++ b/lab/16-annotations-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,72 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/test/java/rewards/internal/StubAccountRepository.java b/lab/16-annotations-solution/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..b926be2
--- /dev/null
+++ b/lab/16-annotations-solution/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,43 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/16-annotations-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..ce1f820
--- /dev/null
+++ b/lab/16-annotations-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,38 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/test/java/rewards/internal/StubRewardRepository.java b/lab/16-annotations-solution/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..2487aca
--- /dev/null
+++ b/lab/16-annotations-solution/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java b/lab/16-annotations-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
new file mode 100644
index 0000000..b14fdce
--- /dev/null
+++ b/lab/16-annotations-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
@@ -0,0 +1,76 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC restaurant repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRestaurantRepositoryTests {
+
+ private JdbcRestaurantRepository repository;
+
+ @BeforeEach
+ public void setUp() {
+ // simulate the Spring bean initialization lifecycle:
+
+ // first, construct the bean
+ repository = new JdbcRestaurantRepository();
+
+ // then, inject its dependencies
+ repository.setDataSource(createTestDataSource());
+
+ // lastly, initialize the bean
+ repository.populateRestaurantCache();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // simulate the Spring bean destruction lifecycle:
+
+ // destroy the bean
+ repository.clearRestaurantCache();
+ }
+
+ @Test
+ public void findRestaurantByMerchantNumber() {
+ Restaurant restaurant = repository.findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "restaurant is null - repository cache not likely initialized");
+ assertEquals("1234567890", restaurant.getNumber(), "number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "name is wrong");
+ assertEquals(Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "benefitPercentage is wrong");
+ }
+
+ @Test
+ public void findRestaurantByBogusMerchantNumber() {
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("bogus");
+ });
+ }
+
+ @Test
+ public void restaurantCacheClearedAfterDestroy() {
+ // force early tear down
+ tearDown();
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("1234567890");
+ });
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/16-annotations/build.gradle b/lab/16-annotations/build.gradle
new file mode 100644
index 0000000..4d6c184
--- /dev/null
+++ b/lab/16-annotations/build.gradle
@@ -0,0 +1,7 @@
+/*
+ * This file was generated by the Gradle 'init' task.
+ */
+
+dependencies {
+ implementation project(':00-rewards-common')
+}
diff --git a/lab/16-annotations/pom.xml b/lab/16-annotations/pom.xml
new file mode 100644
index 0000000..cd95033
--- /dev/null
+++ b/lab/16-annotations/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+ 16-annotations
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
diff --git a/lab/16-annotations/src/main/java/config/RewardsConfig.java b/lab/16-annotations/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..0f30b0f
--- /dev/null
+++ b/lab/16-annotations/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,63 @@
+package config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import rewards.RewardNetwork;
+import rewards.internal.RewardNetworkImpl;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.account.JdbcAccountRepository;
+import rewards.internal.restaurant.JdbcRestaurantRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.JdbcRewardRepository;
+import rewards.internal.reward.RewardRepository;
+
+import javax.sql.DataSource;
+
+/**
+ * TODO-07: Perform component-scanning and run the test again
+ * - Add an appropriate annotation to this class to cause component scanning.
+ * - Set the base package to pick up all the classes we have annotated so far.
+ * - Save all changes, Re-run the RewardNetworkTests. It should now pass.
+ */
+@Configuration
+public class RewardsConfig {
+
+ final DataSource dataSource;
+
+ public RewardsConfig(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ @Bean
+ public RewardNetwork rewardNetwork(){
+ return new RewardNetworkImpl(
+ accountRepository(),
+ restaurantRepository(),
+ rewardRepository());
+ }
+
+ @Bean
+ public AccountRepository accountRepository(){
+ JdbcAccountRepository repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RestaurantRepository restaurantRepository(){
+ JdbcRestaurantRepository repository = new JdbcRestaurantRepository(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RewardRepository rewardRepository(){
+ JdbcRewardRepository repository = new JdbcRewardRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ // TODO-02: Remove all of the @Bean methods above.
+ // - Remove the code that autowires DataSource as well.
+ // - Run the RewardNetworkTests test. It should fail. Why?
+
+}
diff --git a/lab/16-annotations/src/main/java/rewards/AccountContribution.java b/lab/16-annotations/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/Dining.java b/lab/16-annotations/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/RewardConfirmation.java b/lab/16-annotations/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/RewardNetwork.java b/lab/16-annotations/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/16-annotations/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..d8e5442
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,64 @@
+package rewards.internal;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This class is an
+ * application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits
+ * to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account
+ * for dining" use case.
+ */
+
+/* TODO-03: Let this class to be found in component-scanning
+ * - Annotate this class with an appropriate stereotype annotation
+ * to cause component-scanning to create a Spring bean from this class.
+ * - Inject all 3 dependencies. Decide if you should use field
+ * injection or constructor injection.
+ */
+
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/internal/account/Account.java b/lab/16-annotations/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..7b48fa3
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,141 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account.
+ *
+ * Callers should not attempt to hold on or modify the returned set. This method should only be used transitively;
+ * for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/internal/account/AccountRepository.java b/lab/16-annotations/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/internal/account/Beneficiary.java b/lab/16-annotations/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/16-annotations/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..2a2396c
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,133 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+
+/* TODO-05: Let this class to be found in component-scanning
+ * - Annotate the class with an appropriate stereotype annotation
+ * to cause component-scan to detect and load this bean.
+ * - Inject dataSource by annotating setDataSource() method
+ * with @Autowired.
+ */
+
+public class JdbcAccountRepository implements AccountRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to load accounts.
+ *
+ * @param dataSource the data source
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = "select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?";
+ Account account = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, creditCardNumber);
+ rs = ps.executeQuery();
+ account = mapAccount(rs);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred finding by credit card number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
+ for (Beneficiary beneficiary : account.getBeneficiaries()) {
+ ps.setBigDecimal(1, beneficiary.getSavings().asBigDecimal());
+ ps.setLong(2, account.getEntityId());
+ ps.setString(3, beneficiary.getName());
+ ps.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred updating beneficiary savings", e);
+ }
+ // Close to prevent database cursor exhaustion
+ // Close to prevent database connection exhaustion
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY
+ * to a fully-reconstituted Account aggregate.
+ *
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+}
diff --git a/lab/16-annotations/src/main/java/rewards/internal/account/package.html b/lab/16-annotations/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/lab/16-annotations/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/16-annotations/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..11435f7
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,163 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ *
+ * This implementation should cache restaurants to improve performance. The
+ * cache should be populated on initialization and cleared on destruction.
+ */
+
+/* TODO-06: Let this class to be found in component-scanning
+ * - Annotate the class with an appropriate stereotype annotation
+ * to cause component-scanning to detect and load this bean.
+ * - Inject dataSource. Use constructor injection in this case.
+ * Note that there are already two constructors, one of which
+ * is no-arg constructor.
+ */
+
+/*
+ * TODO-08: Use Setter injection for DataSource
+ * - Change the configuration to set the dataSource
+ * property using setDataSource().
+ *
+ * To do this, you must MOVE the @Autowired annotation
+ * you might have set in the previous step on the
+ * constructor injecting DataSource.
+ * So neither constructor should be annotated with
+ * @Autowired now, so Spring uses
+ * the default constructor by default.
+ *
+ * - Re-run the test. It should fail.
+ * - Examine the stack trace and see if you can
+ * understand why. (If not, refer to lab document).
+ * We will fix this error in the next step.
+ */
+
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * The Restaurant object cache. Cached restaurants are indexed
+ * by their merchant numbers.
+ */
+ private Map restaurantCache;
+
+ /**
+ * The constructor sets the data source this repository will use to load
+ * restaurants. When the instance of JdbcRestaurantRepository is created, a
+ * Restaurant cache is populated for read only access
+ */
+
+ public JdbcRestaurantRepository(DataSource dataSource) {
+ this.dataSource = dataSource;
+ this.populateRestaurantCache();
+ }
+
+ public JdbcRestaurantRepository() {
+ }
+
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ return queryRestaurantCache(merchantNumber);
+ }
+
+ /**
+ * Helper method that populates the restaurantCache restaurant object
+ * caches from the rows in the T_RESTAURANT table. Cached restaurants are indexed
+ * by their merchant numbers. This method should be called on initialization.
+ */
+
+ /*
+ * TODO-09: Make this method to be invoked after a bean gets created
+ * - Mark this method with an annotation that will cause it to be
+ * executed by Spring after constructor & setter initialization has occurred.
+ * - Re-run the RewardNetworkTests test. You should see the test succeeds.
+ * - Note that populating the cache is not really a valid
+ * construction activity, so using a post-construct, rather than
+ * the constructor, is a better practice.
+ */
+
+ void populateRestaurantCache() {
+ restaurantCache = new HashMap<>();
+ String sql = "select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE from T_RESTAURANT";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ResultSet rs = ps.executeQuery()) {
+ while (rs.next()) {
+ Restaurant restaurant = mapRestaurant(rs);
+ // index the restaurant by its merchant number
+ restaurantCache.put(restaurant.getNumber(), restaurant);
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred finding by merchant number", e);
+ }
+ }
+
+ /**
+ * Helper method that simply queries the cache of restaurants.
+ *
+ * @param merchantNumber
+ * the restaurant's merchant number
+ * @return the restaurant
+ * @throws EmptyResultDataAccessException
+ * if no restaurant was found with that merchant number
+ */
+ private Restaurant queryRestaurantCache(String merchantNumber) {
+ Restaurant restaurant = restaurantCache.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+
+ /**
+ * Helper method that clears the cache of restaurants.
+ * This method should be called when a bean is destroyed.
+ *
+ * TODO-10: Add a scheme to check if this method is being invoked
+ * - Add System.out.println to this method.
+ *
+ * TODO-11: Have this method to be invoked before a bean gets destroyed
+ * - Re-run RewardNetworkTests.
+ * - Observe this method is not called.
+ * - Use an appropriate annotation to register this method for a
+ * destruction lifecycle callback.
+ * - Re-run the test and you should be able to see
+ * that this method is now being called.
+ */
+ public void clearRestaurantCache() {
+ restaurantCache.clear();
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ *
+ * @param rs
+ * the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+ // map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/16-annotations/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..aa642ae
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,79 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on a benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/16-annotations/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/16-annotations/src/main/java/rewards/internal/restaurant/package.html b/lab/16-annotations/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/16-annotations/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/16-annotations/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..4c7fb73
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,68 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+import javax.sql.DataSource;
+import java.sql.*;
+
+/**
+ * JDBC implementation of a reward repository that
+ * records the result of a reward transaction by
+ * inserting a reward confirmation record.
+ */
+
+/* TODO-04: Let this class to be found in component-scanning
+ * - Annotate the class with an appropriate stereotype annotation
+ * to cause component-scanning to detect and load this bean.
+ * - Inject dataSource by annotating setDataSource() method
+ * with @Autowired.
+ */
+
+public class JdbcRewardRepository implements RewardRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to insert rewards.
+ * @param dataSource the data source
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) {
+ String confirmationNumber = nextConfirmationNumber();
+ ps.setString(1, confirmationNumber);
+ ps.setBigDecimal(2, contribution.getAmount().asBigDecimal());
+ ps.setDate(3, new Date(SimpleDate.today().inMilliseconds()));
+ ps.setString(4, contribution.getAccountNumber());
+ ps.setString(5, dining.getMerchantNumber());
+ ps.setDate(6, new Date(dining.getDate().inMilliseconds()));
+ ps.setBigDecimal(7, dining.getAmount().asBigDecimal());
+ ps.execute();
+ return new RewardConfirmation(confirmationNumber, contribution);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred inserting reward record", e);
+ }
+ // Close to prevent database cursor exhaustion
+ // Close to prevent database connection exhaustion
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) {
+ rs.next();
+ return rs.getString(1);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception getting next confirmation number", e);
+ }
+ // Close to prevent database cursor exhaustion
+ // Close to prevent database cursor exhaustion
+ // Close to prevent database connection exhaustion
+ }
+}
diff --git a/lab/16-annotations/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/16-annotations/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..1207f0f
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/main/java/rewards/internal/reward/package.html b/lab/16-annotations/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/16-annotations/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/16-annotations/src/test/java/rewards/RewardNetworkTests.java b/lab/16-annotations/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..e1f85a2
--- /dev/null
+++ b/lab/16-annotations/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,75 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ApplicationContext;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A system test that verifies the components of the RewardNetwork
+ * application work together to reward for dining successfully.
+ * It uses Spring to bootstrap the application for use in a test environment.
+ *
+ * TODO-00: In this lab, you are going to exercise the following:
+ * - Refactoring the current code that uses Spring configuration with
+ * @Bean methods so that it uses annotation and component-scanning instead
+ * - Using constructor injection and setter injection
+ * - Using @PostConstruct and @PreDestroy
+ *
+ * TODO-01: Run this test before making any changes.
+ * - It should pass.
+ * Note that this test passes only when all the required
+ * beans are correctly configured.
+ */
+public class RewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetwork rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // Create application context from TestInfrastructureConfig,
+ // which also imports RewardsConfig
+ ApplicationContext context = SpringApplication.run(TestInfrastructureConfig.class);
+
+ // Get rewardNetwork bean from the application context
+ rewardNetwork = context.getBean(RewardNetwork.class);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ // this fails if you have selected an account without beneficiaries!
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
diff --git a/lab/16-annotations/src/test/java/rewards/TestInfrastructureConfig.java b/lab/16-annotations/src/test/java/rewards/TestInfrastructureConfig.java
new file mode 100644
index 0000000..bd29e01
--- /dev/null
+++ b/lab/16-annotations/src/test/java/rewards/TestInfrastructureConfig.java
@@ -0,0 +1,28 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import config.RewardsConfig;
+
+@Configuration
+@Import(RewardsConfig.class)
+public class TestInfrastructureConfig {
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/16-annotations/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/16-annotations/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..e2677d4
--- /dev/null
+++ b/lab/16-annotations/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,71 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import common.money.MonetaryAmount;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/test/java/rewards/internal/StubAccountRepository.java b/lab/16-annotations/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..b926be2
--- /dev/null
+++ b/lab/16-annotations/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,43 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/16-annotations/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..ce1f820
--- /dev/null
+++ b/lab/16-annotations/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,38 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/test/java/rewards/internal/StubRewardRepository.java b/lab/16-annotations/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..2487aca
--- /dev/null
+++ b/lab/16-annotations/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/16-annotations/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java b/lab/16-annotations/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
new file mode 100644
index 0000000..1904001
--- /dev/null
+++ b/lab/16-annotations/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
@@ -0,0 +1,73 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC restaurant repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRestaurantRepositoryTests {
+
+ private JdbcRestaurantRepository repository;
+
+ @BeforeEach
+ public void setUp() {
+ // simulate the Spring bean initialization lifecycle:
+ // first, construct the bean
+ repository = new JdbcRestaurantRepository();
+
+ // then, inject its dependencies
+ repository.setDataSource(createTestDataSource());
+
+ // lastly, initialize the bean
+ repository.populateRestaurantCache();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // simulate the Spring bean destruction lifecycle:
+ repository.clearRestaurantCache();
+ }
+
+ @Test
+ public void findRestaurantByMerchantNumber() {
+ Restaurant restaurant = repository.findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "restaurant is null - check your repositories cache");
+ assertEquals("1234567890", restaurant.getNumber(),"number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "name is wrong");
+ assertEquals(Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "benefitPercentage is wrong");
+ }
+
+ @Test
+ public void findRestaurantByBogusMerchantNumber() {
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("bogus");
+ });
+ }
+
+ @Test
+ public void restaurantCacheClearedAfterDestroy() {
+ // force early tear down
+ tearDown();
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("1234567890");
+ });
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/22-aop-solution/build.gradle b/lab/22-aop-solution/build.gradle
new file mode 100644
index 0000000..f1e3161
--- /dev/null
+++ b/lab/22-aop-solution/build.gradle
@@ -0,0 +1,6 @@
+dependencies {
+ implementation project(':00-rewards-common')
+ implementation "org.springframework.boot:spring-boot-starter-aop:$springBootVersion"
+ implementation "org.easymock:easymock:$easyMockVersion"
+ implementation "com.jamonapi:jamon:$jmonVersion"
+}
diff --git a/lab/22-aop-solution/pom.xml b/lab/22-aop-solution/pom.xml
new file mode 100644
index 0000000..84af378
--- /dev/null
+++ b/lab/22-aop-solution/pom.xml
@@ -0,0 +1,33 @@
+
+
+ 4.0.0
+ 22-aop-solution
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+ org.springframework
+ spring-aspects
+
+
+ org.easymock
+ easymock
+
+
+ com.jamonapi
+ jamon
+
+
+
diff --git a/lab/22-aop-solution/src/main/java/config/AspectsConfig.java b/lab/22-aop-solution/src/main/java/config/AspectsConfig.java
new file mode 100644
index 0000000..5bbf13b
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/config/AspectsConfig.java
@@ -0,0 +1,21 @@
+package config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+import rewards.internal.monitor.MonitorFactory;
+import rewards.internal.monitor.jamon.JamonMonitorFactory;
+
+@Configuration
+@ComponentScan(basePackages="rewards.internal.aspects")
+@EnableAspectJAutoProxy
+public class AspectsConfig {
+
+ @Bean
+ public MonitorFactory monitorFactory(){
+ return new JamonMonitorFactory();
+ }
+
+}
diff --git a/lab/22-aop-solution/src/main/java/config/RewardsConfig.java b/lab/22-aop-solution/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..4a06fbc
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,53 @@
+package config;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import rewards.RewardNetwork;
+import rewards.internal.RewardNetworkImpl;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.account.JdbcAccountRepository;
+import rewards.internal.restaurant.JdbcRestaurantRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.JdbcRewardRepository;
+import rewards.internal.reward.RewardRepository;
+
+@Configuration
+public class RewardsConfig {
+
+ @Autowired
+ DataSource dataSource;
+
+ @Bean
+ public RewardNetwork rewardNetwork(){
+ return new RewardNetworkImpl(
+ accountRepository(),
+ restaurantRepository(),
+ rewardRepository());
+ }
+
+ @Bean
+ public AccountRepository accountRepository(){
+ JdbcAccountRepository repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RestaurantRepository restaurantRepository(){
+ JdbcRestaurantRepository repository = new JdbcRestaurantRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RewardRepository rewardRepository(){
+ JdbcRewardRepository repository = new JdbcRewardRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/AccountContribution.java b/lab/22-aop-solution/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/Dining.java b/lab/22-aop-solution/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/RewardConfirmation.java b/lab/22-aop-solution/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/RewardNetwork.java b/lab/22-aop-solution/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/22-aop-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..8de3772
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,52 @@
+package rewards.internal;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.updateReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/account/Account.java b/lab/22-aop-solution/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..84463f0
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,159 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ try {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ } catch (IllegalArgumentException e) {
+ // total would have been over 100% - return invalid
+ return false;
+ }
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account. Callers should not attempt to hold on or modify the returned set.
+ * This method should only be used transitively; for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Returns a single account beneficiary. Callers should not attempt to hold on or modify the returned object. This
+ * method should only be used transitively; for example, called to facilitate reporting or testing.
+ * @param name the name of the beneficiary e.g "Annabelle"
+ * @return the beneficiary object
+ */
+ public Beneficiary getBeneficiary(String name) {
+ for (Beneficiary b : beneficiaries) {
+ if (b.getName().equals(name)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("No such beneficiary with name '" + name + "'");
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/account/AccountRepository.java b/lab/22-aop-solution/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/account/Beneficiary.java b/lab/22-aop-solution/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/22-aop-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..f1f009a
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,125 @@
+package rewards.internal.account;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.exception.RewardDataAccessException;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+public class JdbcAccountRepository implements AccountRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to load accounts.
+ * @param dataSource the data source
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = "select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?";
+ Account account = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, creditCardNumber);
+ rs = ps.executeQuery();
+ account = mapAccount(rs);
+ } catch (SQLException e) {
+ throw new RewardDataAccessException("SQL exception occurred finding by credit card number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ for (Beneficiary beneficiary : account.getBeneficiaries()) {
+ ps.setBigDecimal(1, beneficiary.getSavings().asBigDecimal());
+ ps.setLong(2, account.getEntityId());
+ ps.setString(3, beneficiary.getName());
+ ps.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new RewardDataAccessException("SQL exception occurred updating beneficiary savings", e);
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/account/package.html b/lab/22-aop-solution/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Account module.
+
+
+
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/aspects/DBExceptionHandlingAspect.java b/lab/22-aop-solution/src/main/java/rewards/internal/aspects/DBExceptionHandlingAspect.java
new file mode 100644
index 0000000..f5cadff
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/aspects/DBExceptionHandlingAspect.java
@@ -0,0 +1,26 @@
+package rewards.internal.aspects;
+
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import rewards.internal.exception.RewardDataAccessException;
+
+
+@Aspect
+@Component
+public class DBExceptionHandlingAspect {
+
+ public static final String EMAIL_FAILURE_MSG = "Failed sending an email to Mister Smith : ";
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ @AfterThrowing(value="execution(public * rewards.internal.*.*Repository.*(..))", throwing="e")
+ public void implExceptionHandling(RewardDataAccessException e) {
+ // Log a failure warning
+ logger.warn(EMAIL_FAILURE_MSG + e + "\n");
+ }
+
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/aspects/LoggingAspect.java b/lab/22-aop-solution/src/main/java/rewards/internal/aspects/LoggingAspect.java
new file mode 100644
index 0000000..c4a4ac9
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/aspects/LoggingAspect.java
@@ -0,0 +1,57 @@
+package rewards.internal.aspects;
+
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import rewards.internal.monitor.Monitor;
+import rewards.internal.monitor.MonitorFactory;
+
+@Aspect
+@Component
+public class LoggingAspect {
+ public final static String BEFORE = "'Before'";
+ public final static String AROUND = "'Around'";
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+ private final MonitorFactory monitorFactory;
+
+ public LoggingAspect(MonitorFactory monitorFactory) {
+ this.monitorFactory = monitorFactory;
+ }
+
+ @Before("execution(public * rewards.internal.*.*Repository.find*(..))")
+ public void implLogging(JoinPoint joinPoint) {
+ logger.info(BEFORE + " advice implementation - " + joinPoint.getTarget().getClass() + //
+ "; Executing before " + joinPoint.getSignature().getName() + //
+ "() method");
+
+ }
+
+ @Around("execution(public * rewards.internal.*.*Repository.update*(..))")
+ public Object monitor(ProceedingJoinPoint repositoryMethod) throws Throwable {
+ String name = createJoinPointTraceName(repositoryMethod);
+ Monitor monitor = monitorFactory.start(name);
+ try {
+ return repositoryMethod.proceed();
+ } finally {
+ monitor.stop();
+ logger.info(AROUND + " advice implementation - " + monitor);
+
+ }
+ }
+
+ private String createJoinPointTraceName(JoinPoint joinPoint) {
+ Signature signature = joinPoint.getSignature();
+ StringBuilder sb = new StringBuilder();
+ sb.append(signature.getDeclaringType().getSimpleName());
+ sb.append('.').append(signature.getName());
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/exception/RewardDataAccessException.java b/lab/22-aop-solution/src/main/java/rewards/internal/exception/RewardDataAccessException.java
new file mode 100644
index 0000000..73b2b83
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/exception/RewardDataAccessException.java
@@ -0,0 +1,23 @@
+package rewards.internal.exception;
+
+
+@SuppressWarnings("serial")
+public class RewardDataAccessException extends RuntimeException{
+
+ public RewardDataAccessException() {
+ super();
+ }
+
+ public RewardDataAccessException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public RewardDataAccessException(String message) {
+ super(message);
+ }
+
+ public RewardDataAccessException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/monitor/GlobalMonitorStatistics.java b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/GlobalMonitorStatistics.java
new file mode 100644
index 0000000..ff95636
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/GlobalMonitorStatistics.java
@@ -0,0 +1,24 @@
+package rewards.internal.monitor;
+
+import java.util.Date;
+
+public interface GlobalMonitorStatistics {
+
+ long getCallsCount();
+
+ long getTotalCallTime();
+
+ Date getLastAccessTime();
+
+ long lastCallTime(String methodName);
+
+ long callCount(String methodName);
+
+ long averageCallTime(String methodName);
+
+ long totalCallTime(String methodName);
+
+ long minimumCallTime(String methodName);
+
+ long maximumCallTime(String methodName);
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/monitor/Monitor.java b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/Monitor.java
new file mode 100644
index 0000000..2570ccb
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/Monitor.java
@@ -0,0 +1,8 @@
+package rewards.internal.monitor;
+
+public interface Monitor {
+
+ Monitor start();
+
+ Monitor stop();
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/monitor/MonitorFactory.java b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/MonitorFactory.java
new file mode 100644
index 0000000..ba07f4a
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/MonitorFactory.java
@@ -0,0 +1,6 @@
+package rewards.internal.monitor;
+
+public interface MonitorFactory {
+
+ Monitor start(String name);
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/monitor/MonitorStatistics.java b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/MonitorStatistics.java
new file mode 100644
index 0000000..02b9a00
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/MonitorStatistics.java
@@ -0,0 +1,19 @@
+package rewards.internal.monitor;
+
+public interface MonitorStatistics {
+
+ String getName();
+
+ long getLastCallTime();
+
+ long getCallCount();
+
+ long getAverageCallTime();
+
+ long getTotalCallTime();
+
+ long getMinimumCallTime();
+
+ long getMaximumCallTime();
+
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/monitor/jamon/JamonMonitor.java b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/jamon/JamonMonitor.java
new file mode 100644
index 0000000..4c40cce
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/jamon/JamonMonitor.java
@@ -0,0 +1,63 @@
+package rewards.internal.monitor.jamon;
+
+import rewards.internal.monitor.Monitor;
+import rewards.internal.monitor.MonitorStatistics;
+
+public class JamonMonitor implements Monitor, MonitorStatistics {
+
+ private final com.jamonapi.Monitor monitor;
+
+ public JamonMonitor(com.jamonapi.Monitor monitor) {
+ this.monitor = monitor;
+ }
+
+ public Monitor start() {
+ monitor.start();
+ return this;
+ }
+
+ public Monitor stop() {
+ monitor.stop();
+ return this;
+ }
+
+ public String getName() {
+ return monitor.getLabel();
+ }
+
+ public long getCallCount() {
+ return (long) monitor.getHits();
+ }
+
+ public long getAverageCallTime() {
+ return (long) monitor.getAvg();
+ }
+
+ public long getLastCallTime() {
+ return (long) monitor.getLastValue();
+ }
+
+ public long getMaximumCallTime() {
+ return (long) monitor.getMax();
+ }
+
+ public long getMinimumCallTime() {
+ return (long) monitor.getMin();
+ }
+
+ public long getTotalCallTime() {
+ return (long) monitor.getTotal();
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(monitor.getLabel()).append(": ");
+ sb.append("Last=").append(monitor.getLastValue()).append(", ");
+ sb.append("Calls=").append(monitor.getHits()).append(", ");
+ sb.append("Avg=").append(monitor.getAvg()).append(", ");
+ sb.append("Total=").append(monitor.getTotal()).append(", ");
+ sb.append("Min=").append(monitor.getMin()).append(", ");
+ sb.append("Max=").append(monitor.getMax());
+ return sb.toString();
+ }
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/monitor/jamon/JamonMonitorFactory.java b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/jamon/JamonMonitorFactory.java
new file mode 100644
index 0000000..fbc6690
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/monitor/jamon/JamonMonitorFactory.java
@@ -0,0 +1,59 @@
+package rewards.internal.monitor.jamon;
+
+import java.util.Date;
+
+import rewards.internal.monitor.GlobalMonitorStatistics;
+import rewards.internal.monitor.Monitor;
+import rewards.internal.monitor.MonitorFactory;
+
+import com.jamonapi.MonitorComposite;
+
+public class JamonMonitorFactory implements MonitorFactory, GlobalMonitorStatistics {
+
+ private final com.jamonapi.MonitorFactoryInterface monitorFactory = com.jamonapi.MonitorFactory.getFactory();
+
+ public Monitor start(String name) {
+ return new JamonMonitor(monitorFactory.start(name));
+ }
+
+ public long getCallsCount() {
+ return (long) getMonitors().getHits();
+ }
+
+ public long getTotalCallTime() {
+ return (long) getMonitors().getTotal();
+ }
+
+ public Date getLastAccessTime() {
+ return getMonitors().getLastAccess();
+ }
+
+ public MonitorComposite getMonitors() {
+ return monitorFactory.getRootMonitor();
+ }
+
+ public long averageCallTime(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getAvg();
+ }
+
+ public long callCount(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getHits();
+ }
+
+ public long lastCallTime(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getLastValue();
+ }
+
+ public long maximumCallTime(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getMax();
+ }
+
+ public long minimumCallTime(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getMin();
+ }
+
+ public long totalCallTime(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getTotal();
+ }
+
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/package.html b/lab/22-aop-solution/src/main/java/rewards/internal/package.html
new file mode 100644
index 0000000..8d14d1b
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/package.html
@@ -0,0 +1,7 @@
+
+
+
+The implementation of the rewards application.
+
+
+
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java b/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
new file mode 100644
index 0000000..b7d6d74
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
@@ -0,0 +1,20 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * Determines if benefit is available for an account for dining.
+ *
+ * A value object. A strategy. Scoped by the Resturant aggregate.
+ */
+public interface BenefitAvailabilityPolicy {
+
+ /**
+ * Calculates if an account is eligible to receive benefits for a dining.
+ * @param account the account of the member who dined
+ * @param dining the dining event
+ * @return benefit availability status
+ */
+ boolean isBenefitAvailableFor(Account account, Dining dining);
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..a435646
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,157 @@
+package rewards.internal.restaurant;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+import rewards.internal.exception.RewardDataAccessException;
+
+import common.money.Percentage;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ */
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to load restaurants.
+ * @param dataSource the data source
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ String sql = "select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY from T_RESTAURANT where MERCHANT_NUMBER = ?";
+ Restaurant restaurant = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, merchantNumber);
+ rs = ps.executeQuery();
+ advanceToNextRow(rs);
+ restaurant = mapRestaurant(rs);
+ } catch (SQLException e) {
+ throw new RewardDataAccessException("SQL exception occurred finding by merchant number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return restaurant;
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ * @param rs the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+ // map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ restaurant.setBenefitAvailabilityPolicy(mapBenefitAvailabilityPolicy(rs));
+ return restaurant;
+ }
+
+ /**
+ * Advances a ResultSet to the next row and throws an exception if there are no rows.
+ * @param rs the ResultSet to advance
+ * @throws EmptyResultDataAccessException if there is no next row
+ * @throws SQLException
+ */
+ private void advanceToNextRow(ResultSet rs) throws EmptyResultDataAccessException, SQLException {
+ if (!rs.next()) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ }
+
+ /**
+ * Helper method that maps benefit availability policy data in the ResultSet to a fully-configured
+ * {@link BenefitAvailabilityPolicy} object. The key column is 'BENEFIT_AVAILABILITY_POLICY', which is a
+ * discriminator column containing a string code that identifies the type of policy. Currently supported types are:
+ * 'A' for 'always available' and 'N' for 'never available'.
+ *
+ *
+ * More types could be added easily by enhancing this method. For example, 'W' for 'Weekdays only' or 'M' for 'Max
+ * Rewards per Month'. Some of these types might require additional database column values to be configured, for
+ * example a 'MAX_REWARDS_PER_MONTH' data column.
+ *
+ * @param rs the result set used to map the policy object from database column values
+ * @return the matching benefit availability policy
+ * @throws IllegalArgumentException if the mapping could not be performed
+ */
+ private BenefitAvailabilityPolicy mapBenefitAvailabilityPolicy(ResultSet rs) throws SQLException {
+ String policyCode = rs.getString("BENEFIT_AVAILABILITY_POLICY");
+ if ("A".equals(policyCode)) {
+ return AlwaysAvailable.INSTANCE;
+ } else if ("N".equals(policyCode)) {
+ return NeverAvailable.INSTANCE;
+ } else {
+ throw new IllegalArgumentException("Not a supported policy code " + policyCode);
+ }
+ }
+
+ /**
+ * Returns true indicating benefit is always available.
+ */
+ static class AlwaysAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new AlwaysAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+
+ public String toString() {
+ return "alwaysAvailable";
+ }
+ }
+
+ /**
+ * Returns false indicating benefit is never available.
+ */
+ static class NeverAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new NeverAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return false;
+ }
+
+ public String toString() {
+ return "neverAvailable";
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..2e73e57
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,102 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on an availability policy and a
+ * benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ private BenefitAvailabilityPolicy benefitAvailabilityPolicy;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Sets the policy that determines if a dining by an account at this restaurant is eligible for benefit.
+ * @param benefitAvailabilityPolicy the benefit availability policy
+ */
+ public void setBenefitAvailabilityPolicy(BenefitAvailabilityPolicy benefitAvailabilityPolicy) {
+ this.benefitAvailabilityPolicy = benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Returns this restaurant's benefit availability policy.
+ */
+ public BenefitAvailabilityPolicy getBenefitAvailabilityPolicy() {
+ return benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ if (benefitAvailabilityPolicy.isBenefitAvailableFor(account, dining)) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ } else {
+ return MonetaryAmount.zero();
+ }
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage
+ + ", benefitAvailabilityPolicy = " + benefitAvailabilityPolicy;
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/package.html b/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/22-aop-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..69f2552
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,57 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+import javax.sql.DataSource;
+import java.sql.*;
+
+/**
+ * JDBC implementation of a reward repository that records the result of a reward transaction by inserting a reward
+ * confirmation record.
+ */
+public class JdbcRewardRepository implements RewardRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to insert rewards.
+ * @param dataSource the data source
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public RewardConfirmation updateReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ String confirmationNumber = nextConfirmationNumber();
+ ps.setString(1, confirmationNumber);
+ ps.setBigDecimal(2, contribution.getAmount().asBigDecimal());
+ ps.setDate(3, new Date(SimpleDate.today().inMilliseconds()));
+ ps.setString(4, contribution.getAccountNumber());
+ ps.setString(5, dining.getMerchantNumber());
+ ps.setDate(6, new Date(dining.getDate().inMilliseconds()));
+ ps.setBigDecimal(7, dining.getAmount().asBigDecimal());
+ ps.execute();
+ return new RewardConfirmation(confirmationNumber, contribution);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred inserting reward record", e);
+ }
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ResultSet rs = ps.executeQuery()) {
+ rs.next();
+ return rs.getString(1);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception getting next confirmation number", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/22-aop-solution/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..5b7d4df
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation updateReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/main/java/rewards/internal/reward/package.html b/lab/22-aop-solution/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/22-aop-solution/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/22-aop-solution/src/test/java/rewards/CaptureSystemOutput.java b/lab/22-aop-solution/src/test/java/rewards/CaptureSystemOutput.java
new file mode 100644
index 0000000..c2c169c
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/CaptureSystemOutput.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed 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 rewards;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.jupiter.api.extension.ExtensionContext.Store;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
+import org.junit.platform.commons.support.ReflectionSupport;
+
+/**
+ * {@code @CaptureSystemOutput} is a JUnit JUpiter extension for capturing
+ * output to {@code System.out} and {@code System.err} with expectations
+ * supported via Hamcrest matchers.
+ *
+ *
To obtain an instance of {@code OutputCapture}, declare a parameter of type
+ * {@code OutputCapture} in a JUnit Jupiter {@code @Test}, {@code @BeforeEach},
+ * or {@code @AfterEach} method.
+ *
+ *
{@linkplain #expect Expectations} are supported via Hamcrest matchers.
+ *
+ *
To obtain all output to {@code System.out} and {@code System.err}, simply
+ * invoke {@link #toString()}.
+ *
+ * @author Phillip Webb
+ * @author Andy Wilkinson
+ * @author Sam Brannen
+ */
+ static class OutputCapture {
+
+ private final List> matchers = new ArrayList<>();
+
+ private CaptureOutputStream captureOut;
+
+ private CaptureOutputStream captureErr;
+
+ private ByteArrayOutputStream copy;
+
+ void captureOutput() {
+ this.copy = new ByteArrayOutputStream();
+ this.captureOut = new CaptureOutputStream(System.out, this.copy);
+ this.captureErr = new CaptureOutputStream(System.err, this.copy);
+ System.setOut(new PrintStream(this.captureOut));
+ System.setErr(new PrintStream(this.captureErr));
+ }
+
+ void releaseOutput() {
+ System.setOut(this.captureOut.getOriginal());
+ System.setErr(this.captureErr.getOriginal());
+ this.copy = null;
+ }
+
+ private void flush() {
+ try {
+ this.captureOut.flush();
+ this.captureErr.flush();
+ }
+ catch (IOException ex) {
+ // ignore
+ }
+ }
+
+ /**
+ * Verify that the captured output is matched by the supplied {@code matcher}.
+ *
+ *
Verification is performed after the test method has executed.
+ *
+ * @param matcher the matcher
+ */
+ public void expect(Matcher super String> matcher) {
+ this.matchers.add(matcher);
+ }
+
+ /**
+ * Return all captured output to {@code System.out} and {@code System.err}
+ * as a single string.
+ */
+ @Override
+ public String toString() {
+ flush();
+ return this.copy.toString();
+ }
+
+ private static class CaptureOutputStream extends OutputStream {
+
+ private final PrintStream original;
+
+ private final OutputStream copy;
+
+ CaptureOutputStream(PrintStream original, OutputStream copy) {
+ this.original = original;
+ this.copy = copy;
+ }
+
+ PrintStream getOriginal() {
+ return this.original;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ this.copy.write(b);
+ this.original.write(b);
+ this.original.flush();
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ this.copy.write(b, off, len);
+ this.original.write(b, off, len);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ this.copy.flush();
+ this.original.flush();
+ }
+
+ }
+
+ }
+
+}
diff --git a/lab/22-aop-solution/src/test/java/rewards/DBExceptionHandlingAspectTests.java b/lab/22-aop-solution/src/test/java/rewards/DBExceptionHandlingAspectTests.java
new file mode 100644
index 0000000..5bef106
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/DBExceptionHandlingAspectTests.java
@@ -0,0 +1,37 @@
+package rewards;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import rewards.CaptureSystemOutput.OutputCapture;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.aspects.DBExceptionHandlingAspect;
+import rewards.internal.exception.RewardDataAccessException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = { DbExceptionTestConfig.class })
+public class DBExceptionHandlingAspectTests {
+
+ @Autowired
+ AccountRepository repository;
+
+ @Test
+ @CaptureSystemOutput
+ public void testReportException(OutputCapture capture) {
+ assertThrows(RewardDataAccessException.class, () -> {
+ repository.findByCreditCard("1234123412341234");
+ });
+
+ // The error message should have been logged to the console as a warning
+ assertThat(capture.toString(), containsString(DBExceptionHandlingAspect.EMAIL_FAILURE_MSG));
+ }
+
+}
diff --git a/lab/22-aop-solution/src/test/java/rewards/DbExceptionTestConfig.java b/lab/22-aop-solution/src/test/java/rewards/DbExceptionTestConfig.java
new file mode 100644
index 0000000..33b5afd
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/DbExceptionTestConfig.java
@@ -0,0 +1,31 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import config.AspectsConfig;
+import config.RewardsConfig;
+
+
+@Configuration
+@Import({RewardsConfig.class,AspectsConfig.class})
+public class DbExceptionTestConfig {
+
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder()).setName("rewards-dbexception")
+ // No scripts added. This will cause an exception.
+ .build();
+ }
+
+}
diff --git a/lab/22-aop-solution/src/test/java/rewards/LoggingAspectTests.java b/lab/22-aop-solution/src/test/java/rewards/LoggingAspectTests.java
new file mode 100644
index 0000000..82cf892
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/LoggingAspectTests.java
@@ -0,0 +1,33 @@
+package rewards;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import rewards.CaptureSystemOutput.OutputCapture;
+import rewards.internal.account.AccountRepository;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = { SystemTestConfig.class })
+public class LoggingAspectTests {
+
+ @Autowired
+ AccountRepository repository;
+
+ @Test
+ @CaptureSystemOutput
+ public void testLogger(OutputCapture capture){
+ repository.findByCreditCard("1234123412341234");
+
+ // AOP VERIFICATION
+ // LoggingAspect should have output an INFO message to console
+ String consoleOutput = capture.toString();
+ assertTrue(consoleOutput.startsWith("INFO"));
+ assertTrue(consoleOutput.contains("rewards.internal.aspects.LoggingAspect"));
+ }
+}
diff --git a/lab/22-aop-solution/src/test/java/rewards/RewardNetworkTests.java b/lab/22-aop-solution/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..217e4ce
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,104 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import rewards.CaptureSystemOutput.OutputCapture;
+import rewards.internal.aspects.LoggingAspect;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application
+ * work together to reward for dining successfully. Uses Spring to bootstrap the
+ * application for use in a test environment.
+ */
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes={SystemTestConfig.class})
+public class RewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ @Test
+ @CaptureSystemOutput
+ public void testRewardForDining(OutputCapture capture) {
+ // create a new dining of 100.00 charged to credit card
+ // '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2
+ // distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+
+ checkConsoleOutput(capture, 4);
+ }
+
+ /**
+ * Not only must the code run, but the LoggingAspect should generate logging
+ * output to the console.
+ */
+ private void checkConsoleOutput(OutputCapture capture, int expectedMatches) {
+ // AOP VERIFICATION
+ // Expecting 4 lines of output from the LoggingAspect to console
+ String[] consoleOutput = capture.toString().split("\n");
+ int matches = 0;
+
+ for (String line : consoleOutput) {
+ if (line.contains("rewards.internal.aspects.LoggingAspect")) {
+ if (line.contains(LoggingAspect.BEFORE)) {
+ if (line.contains("JdbcAccountRepository") && line.contains("findByCreditCard"))
+ // Before aspect invoked for
+ // JdbcAccountRepository.findByCreditCard
+ matches++;
+ else if (line.contains("JdbcRestaurantRepository") && line.contains("findByMerchantNumber"))
+ // Before aspect invoked for
+ // JdbcRestaurantRepository.findByMerchantNumber
+ matches++;
+ } else if (line.contains(LoggingAspect.AROUND)) {
+ if (line.contains("AccountRepository") && line.contains("updateBeneficiaries"))
+ // Around aspect invoked for
+ // AccountRepository.updateBeneficiaries
+ matches++;
+ else if (line.contains("Around") && line.contains("RewardRepository")
+ && line.contains("updateReward"))
+ // Around aspect invoked for
+ // RewardRepository.updateReward
+ matches++;
+ }
+ }
+ }
+
+ assertEquals(expectedMatches, matches);
+ }
+
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/test/java/rewards/SystemTestConfig.java b/lab/22-aop-solution/src/test/java/rewards/SystemTestConfig.java
new file mode 100644
index 0000000..4808b80
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/SystemTestConfig.java
@@ -0,0 +1,32 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import config.AspectsConfig;
+import config.RewardsConfig;
+
+
+@Configuration
+@Import({RewardsConfig.class,AspectsConfig.class})
+public class SystemTestConfig {
+
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql")
+ .build();
+ }
+
+}
diff --git a/lab/22-aop-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/22-aop-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..98b7353
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,72 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/test/java/rewards/internal/StubAccountRepository.java b/lab/22-aop-solution/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..b926be2
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,43 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/22-aop-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..418516d
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,53 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+import rewards.internal.restaurant.BenefitAvailabilityPolicy;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new AlwaysReturnsTrue());
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy that always returns true. Only useful for testing--a real
+ * availability policy might consider many factors such as the day of week of the dining, or the account's reward
+ * history for the current month.
+ */
+ private static class AlwaysReturnsTrue implements BenefitAvailabilityPolicy {
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/test/java/rewards/internal/StubRewardRepository.java b/lab/22-aop-solution/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..d4871f8
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation updateReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/test/java/rewards/internal/account/AccountTests.java b/lab/22-aop-solution/src/test/java/rewards/internal/account/AccountTests.java
new file mode 100644
index 0000000..4075654
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/internal/account/AccountTests.java
@@ -0,0 +1,57 @@
+package rewards.internal.account;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for the Account class that verify Account behavior works in isolation.
+ */
+public class AccountTests {
+
+ private final Account account = new Account("1", "Keith and Keri Donald");
+
+ @Test
+ public void accountIsValid() {
+ // setup account with a valid set of beneficiaries to prepare for testing
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ assertTrue(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWithNoBeneficiaries() {
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreOver100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreUnder100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("25%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void makeContribution() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ assertEquals(contribution.getAmount(), MonetaryAmount.valueOf("100.00"));
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java b/lab/22-aop-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
new file mode 100644
index 0000000..4326571
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
@@ -0,0 +1,96 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC account repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcAccountRepositoryTests {
+
+ private JdbcAccountRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ }
+
+ @Test
+ public void testFindAccountByCreditCard() {
+ Account account = repository.findByCreditCard("1234123412341234");
+ // assert the returned account contains what you expect given the state of the database
+ assertNotNull(account, "account should never be null");
+ assertEquals(Long.valueOf(0), account.getEntityId(), "wrong entity id");
+ assertEquals("123456789", account.getNumber(), "wrong account number");
+ assertEquals("Keith and Keri Donald", account.getName(), "wrong name");
+ assertEquals(2, account.getBeneficiaries().size(), "wrong beneficiary collection size");
+
+ Beneficiary b1 = account.getBeneficiary("Annabelle");
+ assertNotNull(b1, "Annabelle should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b1.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b1.getAllocationPercentage(), "wrong allocation percentage");
+
+ Beneficiary b2 = account.getBeneficiary("Corgan");
+ assertNotNull(b2, "Corgan should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b2.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b2.getAllocationPercentage(), "wrong allocation percentage");
+ }
+
+ @Test
+ public void testFindAccountByCreditCardNoAccount() {
+ assertThrows(EmptyResultDataAccessException.class, () -> {
+ repository.findByCreditCard("bogus");
+ });
+ }
+
+ @Test
+ public void testUpdateBeneficiaries() throws SQLException {
+ Account account = repository.findByCreditCard("1234123412341234");
+ account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ repository.updateBeneficiaries(account);
+ verifyBeneficiaryTableUpdated();
+ }
+
+ private void verifyBeneficiaryTableUpdated() throws SQLException {
+ String sql = "select SAVINGS from T_ACCOUNT_BENEFICIARY where NAME = ? and ACCOUNT_ID = ?";
+ PreparedStatement stmt = dataSource.getConnection().prepareStatement(sql);
+
+ // assert Annabelle has $4.00 savings now
+ stmt.setString(1, "Annabelle");
+ stmt.setLong(2, 0L);
+ ResultSet rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+
+ // assert Corgan has $4.00 savings now
+ stmt.setString(1, "Corgan");
+ stmt.setLong(2, 0L);
+ rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/22-aop-solution/src/test/java/rewards/internal/aspects/RepositoryPerformanceLogTest.java b/lab/22-aop-solution/src/test/java/rewards/internal/aspects/RepositoryPerformanceLogTest.java
new file mode 100644
index 0000000..996ad44
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/internal/aspects/RepositoryPerformanceLogTest.java
@@ -0,0 +1,36 @@
+package rewards.internal.aspects;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.easymock.EasyMock;
+import org.junit.jupiter.api.Test;
+
+import rewards.internal.monitor.jamon.JamonMonitorFactory;
+
+/**
+ * Unit test to test the behavior of the RepositoryPerformanceMonitor aspect in isolation.
+ */
+public class RepositoryPerformanceLogTest {
+
+ @Test
+ public void testMonitor() throws Throwable {
+ JamonMonitorFactory monitorFactory = new JamonMonitorFactory();
+ LoggingAspect performanceMonitor = new LoggingAspect(monitorFactory);
+ Signature signature = EasyMock.createMock(Signature.class);
+ ProceedingJoinPoint targetMethod = EasyMock.createMock(ProceedingJoinPoint.class);
+
+ expect(targetMethod.getSignature()).andReturn(signature);
+ expect(signature.getDeclaringType()).andReturn(Object.class);
+ expect(signature.getName()).andReturn("hashCode");
+ expect(targetMethod.proceed()).andReturn(new Object());
+
+ replay(signature, targetMethod);
+ performanceMonitor.monitor(targetMethod);
+ verify(signature, targetMethod);
+ }
+
+}
diff --git a/lab/22-aop-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java b/lab/22-aop-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
new file mode 100644
index 0000000..565dc1b
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
@@ -0,0 +1,52 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC restaurant repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRestaurantRepositoryTests {
+
+ private JdbcRestaurantRepository repository;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRestaurantRepository();
+ repository.setDataSource(createTestDataSource());
+ }
+
+ @Test
+ public void testFindRestaurantByMerchantNumber() {
+ Restaurant restaurant = repository.findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "the restaurant should never be null");
+ assertEquals("1234567890", restaurant.getNumber(), "the merchant number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "the name is wrong");
+ assertEquals(Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "the benefitPercentage is wrong");
+ assertEquals(JdbcRestaurantRepository.AlwaysAvailable.INSTANCE,
+ restaurant.getBenefitAvailabilityPolicy(), "the benefit availability policy is wrong");
+ }
+
+ @Test
+ public void testFindRestaurantByBogusMerchantNumber() {
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("bogus");
+ });
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/22-aop-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java b/lab/22-aop-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java
new file mode 100644
index 0000000..93c2496
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java
@@ -0,0 +1,71 @@
+package rewards.internal.restaurant;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for exercising the behavior of the Restaurant aggregate entity. A restaurant calculates a benefit to award
+ * to an account for dining based on an availability policy and benefit percentage.
+ */
+public class RestaurantTests {
+
+ private Restaurant restaurant;
+
+ private Account account;
+
+ private Dining dining;
+
+ @BeforeEach
+ public void setUp() {
+ // configure the restaurant, the object being tested
+ restaurant = new Restaurant("1234567890", "AppleBee's");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(true));
+ // configure supporting objects needed by the restaurant
+ account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle");
+ dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+ }
+
+ @Test
+ public void testCalcuateBenefitFor() {
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert 8.00 eligible for reward
+ assertEquals(MonetaryAmount.valueOf("8.00"), benefit);
+ }
+
+ @Test
+ public void testNoBenefitAvailable() {
+ // configure stub that always returns false
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(false));
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert zero eligible for reward
+ assertEquals(MonetaryAmount.valueOf("0.00"), benefit);
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy containing a single flag used to determine if benefit is available.
+ * Only useful for testing--a real availability policy might consider many factors such as the day of week of the
+ * dining, or the account's reward history for the current month.
+ */
+ private static class StubBenefitAvailibilityPolicy implements BenefitAvailabilityPolicy {
+
+ private final boolean isBenefitAvailable;
+
+ public StubBenefitAvailibilityPolicy(boolean isBenefitAvailable) {
+ this.isBenefitAvailable = isBenefitAvailable;
+ }
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return isBenefitAvailable;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java b/lab/22-aop-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
new file mode 100644
index 0000000..1ffc0a9
--- /dev/null
+++ b/lab/22-aop-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
@@ -0,0 +1,80 @@
+package rewards.internal.reward;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.Account;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Tests the JDBC reward repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRewardRepositoryTests {
+
+ private JdbcRewardRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRewardRepository();
+ dataSource = createTestDataSource();
+ repository.setDataSource(dataSource);
+ }
+
+ @Test public void createReward() throws SQLException {
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "0123456789");
+
+ Account account = new Account("1", "Keith and Keri Donald");
+ account.setEntityId(0L);
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ RewardConfirmation confirmation = repository.updateReward(contribution, dining);
+ assertNotNull(confirmation, "confirmation should not be null");
+ assertNotNull("confirmation number should not be null", confirmation.getConfirmationNumber());
+ assertEquals(contribution, confirmation.getAccountContribution(), "wrong contribution object");
+ verifyRewardInserted(confirmation, dining);
+ }
+
+ private void verifyRewardInserted(RewardConfirmation confirmation, Dining dining) throws SQLException {
+ assertEquals(1, getRewardCount());
+ Statement stmt = dataSource.getConnection().createStatement();
+ ResultSet rs = stmt.executeQuery("select REWARD_AMOUNT from T_REWARD where CONFIRMATION_NUMBER = '"
+ + confirmation.getConfirmationNumber() + "'");
+ rs.next();
+ assertEquals(confirmation.getAccountContribution().getAmount(), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private int getRewardCount() throws SQLException {
+ Statement stmt = dataSource.getConnection().createStatement();
+ ResultSet rs = stmt.executeQuery("select count(*) from T_REWARD");
+ rs.next();
+ return rs.getInt(1);
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/22-aop/build.gradle b/lab/22-aop/build.gradle
new file mode 100644
index 0000000..f1e3161
--- /dev/null
+++ b/lab/22-aop/build.gradle
@@ -0,0 +1,6 @@
+dependencies {
+ implementation project(':00-rewards-common')
+ implementation "org.springframework.boot:spring-boot-starter-aop:$springBootVersion"
+ implementation "org.easymock:easymock:$easyMockVersion"
+ implementation "com.jamonapi:jamon:$jmonVersion"
+}
diff --git a/lab/22-aop/pom.xml b/lab/22-aop/pom.xml
new file mode 100644
index 0000000..119c804
--- /dev/null
+++ b/lab/22-aop/pom.xml
@@ -0,0 +1,50 @@
+
+
+ 4.0.0
+ 22-aop
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+ org.springframework
+ spring-aspects
+
+
+ org.easymock
+ easymock
+
+
+ com.jamonapi
+ jamon
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ **/RewardNetworkTests.java
+ **/DBExceptionHandlingAspectTests.java
+ **/LoggingAspectTests.java
+ **/RepositoryPerformanceLogTest.java
+
+
+
+
+
+
diff --git a/lab/22-aop/src/main/java/config/AspectsConfig.java b/lab/22-aop/src/main/java/config/AspectsConfig.java
new file mode 100644
index 0000000..a97f880
--- /dev/null
+++ b/lab/22-aop/src/main/java/config/AspectsConfig.java
@@ -0,0 +1,25 @@
+package config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import rewards.internal.monitor.MonitorFactory;
+import rewards.internal.monitor.jamon.JamonMonitorFactory;
+
+// TODO-04: Update Aspect related configuration
+// - Add a class-level annotation to scan for components
+// located in the rewards.internal.aspects package.
+// - Add @EnableAspectJAutoProxy to this class to instruct Spring
+// to process beans that have the @Aspect annotation.
+// (Note that this annotation is redundant for Spring Boot
+// application since it will be automatically added through
+// auto configuration.)
+@Configuration
+public class AspectsConfig {
+
+ @Bean
+ public MonitorFactory monitorFactory(){
+ return new JamonMonitorFactory();
+ }
+
+}
diff --git a/lab/22-aop/src/main/java/config/RewardsConfig.java b/lab/22-aop/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..4a06fbc
--- /dev/null
+++ b/lab/22-aop/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,53 @@
+package config;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import rewards.RewardNetwork;
+import rewards.internal.RewardNetworkImpl;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.account.JdbcAccountRepository;
+import rewards.internal.restaurant.JdbcRestaurantRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.JdbcRewardRepository;
+import rewards.internal.reward.RewardRepository;
+
+@Configuration
+public class RewardsConfig {
+
+ @Autowired
+ DataSource dataSource;
+
+ @Bean
+ public RewardNetwork rewardNetwork(){
+ return new RewardNetworkImpl(
+ accountRepository(),
+ restaurantRepository(),
+ rewardRepository());
+ }
+
+ @Bean
+ public AccountRepository accountRepository(){
+ JdbcAccountRepository repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RestaurantRepository restaurantRepository(){
+ JdbcRestaurantRepository repository = new JdbcRestaurantRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RewardRepository rewardRepository(){
+ JdbcRewardRepository repository = new JdbcRewardRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+}
diff --git a/lab/22-aop/src/main/java/rewards/AccountContribution.java b/lab/22-aop/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/Dining.java b/lab/22-aop/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/RewardConfirmation.java b/lab/22-aop/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/RewardNetwork.java b/lab/22-aop/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/22-aop/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..8de3772
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,52 @@
+package rewards.internal;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.updateReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/account/Account.java b/lab/22-aop/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..84463f0
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,159 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ try {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ } catch (IllegalArgumentException e) {
+ // total would have been over 100% - return invalid
+ return false;
+ }
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account. Callers should not attempt to hold on or modify the returned set.
+ * This method should only be used transitively; for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Returns a single account beneficiary. Callers should not attempt to hold on or modify the returned object. This
+ * method should only be used transitively; for example, called to facilitate reporting or testing.
+ * @param name the name of the beneficiary e.g "Annabelle"
+ * @return the beneficiary object
+ */
+ public Beneficiary getBeneficiary(String name) {
+ for (Beneficiary b : beneficiaries) {
+ if (b.getName().equals(name)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("No such beneficiary with name '" + name + "'");
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/account/AccountRepository.java b/lab/22-aop/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/account/Beneficiary.java b/lab/22-aop/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/22-aop/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..274cf35
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,127 @@
+package rewards.internal.account;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+import rewards.internal.exception.RewardDataAccessException;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+public class JdbcAccountRepository implements AccountRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to load accounts.
+ *
+ * @param dataSource the data source
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = "select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?";
+ Account account = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, creditCardNumber);
+ rs = ps.executeQuery();
+ account = mapAccount(rs);
+ } catch (SQLException e) {
+ throw new RewardDataAccessException("SQL exception occurred finding by credit card number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ for (Beneficiary beneficiary : account.getBeneficiaries()) {
+ ps.setBigDecimal(1, beneficiary.getSavings().asBigDecimal());
+ ps.setLong(2, account.getEntityId());
+ ps.setString(3, beneficiary.getName());
+ ps.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new RewardDataAccessException("SQL exception occurred updating beneficiary savings", e);
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ *
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/account/package.html b/lab/22-aop/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Account module.
+
+
+
diff --git a/lab/22-aop/src/main/java/rewards/internal/aspects/DBExceptionHandlingAspect.java b/lab/22-aop/src/main/java/rewards/internal/aspects/DBExceptionHandlingAspect.java
new file mode 100644
index 0000000..681ba83
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/aspects/DBExceptionHandlingAspect.java
@@ -0,0 +1,33 @@
+package rewards.internal.aspects;
+
+import org.aspectj.lang.annotation.Aspect;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import rewards.internal.exception.RewardDataAccessException;
+
+
+@Aspect
+public class DBExceptionHandlingAspect {
+
+ public static final String EMAIL_FAILURE_MSG = "Failed sending an email to Mister Smith : ";
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+
+ // TODO-10 (Optional): Use AOP to log an exception.
+ // (Steps 10, 11 and 12 are optional, skip them if you are short on time)
+ //
+ // - Configure this advice method to enable logging of
+ // exceptions thrown by Repository class methods.
+ // - Select the advice type that seems most appropriate.
+
+ public void implExceptionHandling(RewardDataAccessException e) {
+ // Log a failure warning
+ logger.warn(EMAIL_FAILURE_MSG + e + "\n");
+ }
+
+ // TODO-11 (Optional): Annotate this class as a Spring-managed bean.
+ // - Note that we enabled component scanning in an earlier step.
+
+}
diff --git a/lab/22-aop/src/main/java/rewards/internal/aspects/LoggingAspect.java b/lab/22-aop/src/main/java/rewards/internal/aspects/LoggingAspect.java
new file mode 100644
index 0000000..00a27d4
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/aspects/LoggingAspect.java
@@ -0,0 +1,77 @@
+package rewards.internal.aspects;
+
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import rewards.internal.monitor.Monitor;
+import rewards.internal.monitor.MonitorFactory;
+
+// TODO-02: Use AOP to log a message before
+// any repository's find...() method is invoked.
+// - Add an appropriate annotation to this class to indicate this class is an aspect.
+// - Also make it as a component.
+// - Optionally place @Autowired annotation on the constructor
+// where `MonitorFactory` dependency is being injected.
+// (It is optional since there is only a single constructor in the class.)
+
+public class LoggingAspect {
+ public final static String BEFORE = "'Before'";
+ public final static String AROUND = "'Around'";
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+ private final MonitorFactory monitorFactory;
+
+
+ public LoggingAspect(MonitorFactory monitorFactory) {
+ this.monitorFactory = monitorFactory;
+ }
+
+
+ // TODO-03: Write Pointcut Expression
+ // - Decide which advice type is most appropriate
+ // - Write a pointcut expression that selects only find* methods on
+ // our repository classes.
+
+ public void implLogging(JoinPoint joinPoint) {
+ // Do not modify this log message or the test will fail
+ logger.info(BEFORE + " advice implementation - " + joinPoint.getTarget().getClass() + //
+ "; Executing before " + joinPoint.getSignature().getName() + //
+ "() method");
+ }
+
+
+ // TODO-07: Use AOP to time update...() methods.
+ // - Mark this method as an around advice.
+ // - Write a pointcut expression to match on all update* methods
+ // on all Repository classes.
+
+ public Object monitor(ProceedingJoinPoint repositoryMethod) throws Throwable {
+ String name = createJoinPointTraceName(repositoryMethod);
+ Monitor monitor = monitorFactory.start(name);
+ try {
+ // Invoke repository method ...
+
+ // TODO-08: Add the logic to proceed with the target method invocation.
+ // - Be sure to return the target method's return value to the caller
+ // and delete the line below.
+
+ return new String("Delete this line after completing TODO-08");
+
+ } finally {
+ monitor.stop();
+ // Do not modify this log message or the test will fail
+ logger.info(AROUND + " advice implementation - " + monitor);
+ }
+ }
+
+ private String createJoinPointTraceName(JoinPoint joinPoint) {
+ Signature signature = joinPoint.getSignature();
+ StringBuilder sb = new StringBuilder();
+ sb.append(signature.getDeclaringType().getSimpleName());
+ sb.append('.').append(signature.getName());
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/exception/RewardDataAccessException.java b/lab/22-aop/src/main/java/rewards/internal/exception/RewardDataAccessException.java
new file mode 100644
index 0000000..73b2b83
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/exception/RewardDataAccessException.java
@@ -0,0 +1,23 @@
+package rewards.internal.exception;
+
+
+@SuppressWarnings("serial")
+public class RewardDataAccessException extends RuntimeException{
+
+ public RewardDataAccessException() {
+ super();
+ }
+
+ public RewardDataAccessException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public RewardDataAccessException(String message) {
+ super(message);
+ }
+
+ public RewardDataAccessException(Throwable cause) {
+ super(cause);
+ }
+
+}
diff --git a/lab/22-aop/src/main/java/rewards/internal/monitor/GlobalMonitorStatistics.java b/lab/22-aop/src/main/java/rewards/internal/monitor/GlobalMonitorStatistics.java
new file mode 100644
index 0000000..ff95636
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/monitor/GlobalMonitorStatistics.java
@@ -0,0 +1,24 @@
+package rewards.internal.monitor;
+
+import java.util.Date;
+
+public interface GlobalMonitorStatistics {
+
+ long getCallsCount();
+
+ long getTotalCallTime();
+
+ Date getLastAccessTime();
+
+ long lastCallTime(String methodName);
+
+ long callCount(String methodName);
+
+ long averageCallTime(String methodName);
+
+ long totalCallTime(String methodName);
+
+ long minimumCallTime(String methodName);
+
+ long maximumCallTime(String methodName);
+}
diff --git a/lab/22-aop/src/main/java/rewards/internal/monitor/Monitor.java b/lab/22-aop/src/main/java/rewards/internal/monitor/Monitor.java
new file mode 100644
index 0000000..2570ccb
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/monitor/Monitor.java
@@ -0,0 +1,8 @@
+package rewards.internal.monitor;
+
+public interface Monitor {
+
+ Monitor start();
+
+ Monitor stop();
+}
diff --git a/lab/22-aop/src/main/java/rewards/internal/monitor/MonitorFactory.java b/lab/22-aop/src/main/java/rewards/internal/monitor/MonitorFactory.java
new file mode 100644
index 0000000..ba07f4a
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/monitor/MonitorFactory.java
@@ -0,0 +1,6 @@
+package rewards.internal.monitor;
+
+public interface MonitorFactory {
+
+ Monitor start(String name);
+}
diff --git a/lab/22-aop/src/main/java/rewards/internal/monitor/MonitorStatistics.java b/lab/22-aop/src/main/java/rewards/internal/monitor/MonitorStatistics.java
new file mode 100644
index 0000000..02b9a00
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/monitor/MonitorStatistics.java
@@ -0,0 +1,19 @@
+package rewards.internal.monitor;
+
+public interface MonitorStatistics {
+
+ String getName();
+
+ long getLastCallTime();
+
+ long getCallCount();
+
+ long getAverageCallTime();
+
+ long getTotalCallTime();
+
+ long getMinimumCallTime();
+
+ long getMaximumCallTime();
+
+}
diff --git a/lab/22-aop/src/main/java/rewards/internal/monitor/jamon/JamonMonitor.java b/lab/22-aop/src/main/java/rewards/internal/monitor/jamon/JamonMonitor.java
new file mode 100644
index 0000000..4c40cce
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/monitor/jamon/JamonMonitor.java
@@ -0,0 +1,63 @@
+package rewards.internal.monitor.jamon;
+
+import rewards.internal.monitor.Monitor;
+import rewards.internal.monitor.MonitorStatistics;
+
+public class JamonMonitor implements Monitor, MonitorStatistics {
+
+ private final com.jamonapi.Monitor monitor;
+
+ public JamonMonitor(com.jamonapi.Monitor monitor) {
+ this.monitor = monitor;
+ }
+
+ public Monitor start() {
+ monitor.start();
+ return this;
+ }
+
+ public Monitor stop() {
+ monitor.stop();
+ return this;
+ }
+
+ public String getName() {
+ return monitor.getLabel();
+ }
+
+ public long getCallCount() {
+ return (long) monitor.getHits();
+ }
+
+ public long getAverageCallTime() {
+ return (long) monitor.getAvg();
+ }
+
+ public long getLastCallTime() {
+ return (long) monitor.getLastValue();
+ }
+
+ public long getMaximumCallTime() {
+ return (long) monitor.getMax();
+ }
+
+ public long getMinimumCallTime() {
+ return (long) monitor.getMin();
+ }
+
+ public long getTotalCallTime() {
+ return (long) monitor.getTotal();
+ }
+
+ public String toString() {
+ StringBuilder sb = new StringBuilder();
+ sb.append(monitor.getLabel()).append(": ");
+ sb.append("Last=").append(monitor.getLastValue()).append(", ");
+ sb.append("Calls=").append(monitor.getHits()).append(", ");
+ sb.append("Avg=").append(monitor.getAvg()).append(", ");
+ sb.append("Total=").append(monitor.getTotal()).append(", ");
+ sb.append("Min=").append(monitor.getMin()).append(", ");
+ sb.append("Max=").append(monitor.getMax());
+ return sb.toString();
+ }
+}
diff --git a/lab/22-aop/src/main/java/rewards/internal/monitor/jamon/JamonMonitorFactory.java b/lab/22-aop/src/main/java/rewards/internal/monitor/jamon/JamonMonitorFactory.java
new file mode 100644
index 0000000..fbc6690
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/monitor/jamon/JamonMonitorFactory.java
@@ -0,0 +1,59 @@
+package rewards.internal.monitor.jamon;
+
+import java.util.Date;
+
+import rewards.internal.monitor.GlobalMonitorStatistics;
+import rewards.internal.monitor.Monitor;
+import rewards.internal.monitor.MonitorFactory;
+
+import com.jamonapi.MonitorComposite;
+
+public class JamonMonitorFactory implements MonitorFactory, GlobalMonitorStatistics {
+
+ private final com.jamonapi.MonitorFactoryInterface monitorFactory = com.jamonapi.MonitorFactory.getFactory();
+
+ public Monitor start(String name) {
+ return new JamonMonitor(monitorFactory.start(name));
+ }
+
+ public long getCallsCount() {
+ return (long) getMonitors().getHits();
+ }
+
+ public long getTotalCallTime() {
+ return (long) getMonitors().getTotal();
+ }
+
+ public Date getLastAccessTime() {
+ return getMonitors().getLastAccess();
+ }
+
+ public MonitorComposite getMonitors() {
+ return monitorFactory.getRootMonitor();
+ }
+
+ public long averageCallTime(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getAvg();
+ }
+
+ public long callCount(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getHits();
+ }
+
+ public long lastCallTime(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getLastValue();
+ }
+
+ public long maximumCallTime(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getMax();
+ }
+
+ public long minimumCallTime(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getMin();
+ }
+
+ public long totalCallTime(String methodName) {
+ return (long) monitorFactory.getMonitor(methodName, "ms.").getTotal();
+ }
+
+}
diff --git a/lab/22-aop/src/main/java/rewards/internal/package.html b/lab/22-aop/src/main/java/rewards/internal/package.html
new file mode 100644
index 0000000..8d14d1b
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/package.html
@@ -0,0 +1,7 @@
+
+
+
+The implementation of the rewards application.
+
+
+
diff --git a/lab/22-aop/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java b/lab/22-aop/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
new file mode 100644
index 0000000..b7d6d74
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
@@ -0,0 +1,20 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * Determines if benefit is available for an account for dining.
+ *
+ * A value object. A strategy. Scoped by the Resturant aggregate.
+ */
+public interface BenefitAvailabilityPolicy {
+
+ /**
+ * Calculates if an account is eligible to receive benefits for a dining.
+ * @param account the account of the member who dined
+ * @param dining the dining event
+ * @return benefit availability status
+ */
+ boolean isBenefitAvailableFor(Account account, Dining dining);
+}
diff --git a/lab/22-aop/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/22-aop/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..6ba9720
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,159 @@
+package rewards.internal.restaurant;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+import rewards.internal.exception.RewardDataAccessException;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.Percentage;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ */
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to load restaurants.
+ *
+ * @param dataSource the data source
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ String sql = "select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY from T_RESTAURANT where MERCHANT_NUMBER = ?";
+ Restaurant restaurant = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, merchantNumber);
+ rs = ps.executeQuery();
+ advanceToNextRow(rs);
+ restaurant = mapRestaurant(rs);
+ } catch (SQLException e) {
+ throw new RewardDataAccessException("SQL exception occurred finding by merchant number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return restaurant;
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+ // map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ restaurant.setBenefitAvailabilityPolicy(mapBenefitAvailabilityPolicy(rs));
+ return restaurant;
+ }
+
+ /**
+ * Advances a ResultSet to the next row and throws an exception if there are no rows.
+ * @param rs the ResultSet to advance
+ * @throws EmptyResultDataAccessException if there is no next row
+ * @throws SQLException
+ */
+ private void advanceToNextRow(ResultSet rs) throws EmptyResultDataAccessException, SQLException {
+ if (!rs.next()) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ }
+
+ /**
+ * Helper method that maps benefit availability policy data in the ResultSet to a fully-configured
+ * {@link BenefitAvailabilityPolicy} object. The key column is 'BENEFIT_AVAILABILITY_POLICY', which is a
+ * discriminator column containing a string code that identifies the type of policy. Currently supported types are:
+ * 'A' for 'always available' and 'N' for 'never available'.
+ *
+ *
+ * More types could be added easily by enhancing this method. For example, 'W' for 'Weekdays only' or 'M' for 'Max
+ * Rewards per Month'. Some of these types might require additional database column values to be configured, for
+ * example a 'MAX_REWARDS_PER_MONTH' data column.
+ *
+ * @param rs the result set used to map the policy object from database column values
+ * @return the matching benefit availability policy
+ * @throws IllegalArgumentException if the mapping could not be performed
+ */
+ private BenefitAvailabilityPolicy mapBenefitAvailabilityPolicy(ResultSet rs) throws SQLException {
+ String policyCode = rs.getString("BENEFIT_AVAILABILITY_POLICY");
+ if ("A".equals(policyCode)) {
+ return AlwaysAvailable.INSTANCE;
+ } else if ("N".equals(policyCode)) {
+ return NeverAvailable.INSTANCE;
+ } else {
+ throw new IllegalArgumentException("Not a supported policy code " + policyCode);
+ }
+ }
+
+ /**
+ * Returns true indicating benefit is always available.
+ */
+ static class AlwaysAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new AlwaysAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+
+ public String toString() {
+ return "alwaysAvailable";
+ }
+ }
+
+ /**
+ * Returns false indicating benefit is never available.
+ */
+ static class NeverAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new NeverAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return false;
+ }
+
+ public String toString() {
+ return "neverAvailable";
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/22-aop/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..2e73e57
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,102 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on an availability policy and a
+ * benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ private BenefitAvailabilityPolicy benefitAvailabilityPolicy;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Sets the policy that determines if a dining by an account at this restaurant is eligible for benefit.
+ * @param benefitAvailabilityPolicy the benefit availability policy
+ */
+ public void setBenefitAvailabilityPolicy(BenefitAvailabilityPolicy benefitAvailabilityPolicy) {
+ this.benefitAvailabilityPolicy = benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Returns this restaurant's benefit availability policy.
+ */
+ public BenefitAvailabilityPolicy getBenefitAvailabilityPolicy() {
+ return benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ if (benefitAvailabilityPolicy.isBenefitAvailableFor(account, dining)) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ } else {
+ return MonetaryAmount.zero();
+ }
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage
+ + ", benefitAvailabilityPolicy = " + benefitAvailabilityPolicy;
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/22-aop/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/22-aop/src/main/java/rewards/internal/restaurant/package.html b/lab/22-aop/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/22-aop/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/22-aop/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..69f2552
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,57 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+import javax.sql.DataSource;
+import java.sql.*;
+
+/**
+ * JDBC implementation of a reward repository that records the result of a reward transaction by inserting a reward
+ * confirmation record.
+ */
+public class JdbcRewardRepository implements RewardRepository {
+
+ private DataSource dataSource;
+
+ /**
+ * Sets the data source this repository will use to insert rewards.
+ * @param dataSource the data source
+ */
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public RewardConfirmation updateReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ String confirmationNumber = nextConfirmationNumber();
+ ps.setString(1, confirmationNumber);
+ ps.setBigDecimal(2, contribution.getAmount().asBigDecimal());
+ ps.setDate(3, new Date(SimpleDate.today().inMilliseconds()));
+ ps.setString(4, contribution.getAccountNumber());
+ ps.setString(5, dining.getMerchantNumber());
+ ps.setDate(6, new Date(dining.getDate().inMilliseconds()));
+ ps.setBigDecimal(7, dining.getAmount().asBigDecimal());
+ ps.execute();
+ return new RewardConfirmation(confirmationNumber, contribution);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred inserting reward record", e);
+ }
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ResultSet rs = ps.executeQuery()) {
+ rs.next();
+ return rs.getString(1);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception getting next confirmation number", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/22-aop/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..5b7d4df
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation updateReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/main/java/rewards/internal/reward/package.html b/lab/22-aop/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/22-aop/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/22-aop/src/test/java/rewards/CaptureSystemOutput.java b/lab/22-aop/src/test/java/rewards/CaptureSystemOutput.java
new file mode 100644
index 0000000..c2c169c
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/CaptureSystemOutput.java
@@ -0,0 +1,244 @@
+/*
+ * Copyright 2012-2017 the original author or authors.
+ *
+ * Licensed 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 rewards;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.allOf;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.hamcrest.Matcher;
+import org.junit.jupiter.api.extension.AfterEachCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.api.extension.ExtensionContext;
+import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
+import org.junit.jupiter.api.extension.ExtensionContext.Store;
+import org.junit.jupiter.api.extension.ParameterContext;
+import org.junit.jupiter.api.extension.ParameterResolver;
+import org.junit.platform.commons.support.ReflectionSupport;
+
+/**
+ * {@code @CaptureSystemOutput} is a JUnit JUpiter extension for capturing
+ * output to {@code System.out} and {@code System.err} with expectations
+ * supported via Hamcrest matchers.
+ *
+ *
To obtain an instance of {@code OutputCapture}, declare a parameter of type
+ * {@code OutputCapture} in a JUnit Jupiter {@code @Test}, {@code @BeforeEach},
+ * or {@code @AfterEach} method.
+ *
+ *
{@linkplain #expect Expectations} are supported via Hamcrest matchers.
+ *
+ *
To obtain all output to {@code System.out} and {@code System.err}, simply
+ * invoke {@link #toString()}.
+ *
+ * @author Phillip Webb
+ * @author Andy Wilkinson
+ * @author Sam Brannen
+ */
+ static class OutputCapture {
+
+ private final List> matchers = new ArrayList<>();
+
+ private CaptureOutputStream captureOut;
+
+ private CaptureOutputStream captureErr;
+
+ private ByteArrayOutputStream copy;
+
+ void captureOutput() {
+ this.copy = new ByteArrayOutputStream();
+ this.captureOut = new CaptureOutputStream(System.out, this.copy);
+ this.captureErr = new CaptureOutputStream(System.err, this.copy);
+ System.setOut(new PrintStream(this.captureOut));
+ System.setErr(new PrintStream(this.captureErr));
+ }
+
+ void releaseOutput() {
+ System.setOut(this.captureOut.getOriginal());
+ System.setErr(this.captureErr.getOriginal());
+ this.copy = null;
+ }
+
+ private void flush() {
+ try {
+ this.captureOut.flush();
+ this.captureErr.flush();
+ }
+ catch (IOException ex) {
+ // ignore
+ }
+ }
+
+ /**
+ * Verify that the captured output is matched by the supplied {@code matcher}.
+ *
+ *
Verification is performed after the test method has executed.
+ *
+ * @param matcher the matcher
+ */
+ public void expect(Matcher super String> matcher) {
+ this.matchers.add(matcher);
+ }
+
+ /**
+ * Return all captured output to {@code System.out} and {@code System.err}
+ * as a single string.
+ */
+ @Override
+ public String toString() {
+ flush();
+ return this.copy.toString();
+ }
+
+ private static class CaptureOutputStream extends OutputStream {
+
+ private final PrintStream original;
+
+ private final OutputStream copy;
+
+ CaptureOutputStream(PrintStream original, OutputStream copy) {
+ this.original = original;
+ this.copy = copy;
+ }
+
+ PrintStream getOriginal() {
+ return this.original;
+ }
+
+ @Override
+ public void write(int b) throws IOException {
+ this.copy.write(b);
+ this.original.write(b);
+ this.original.flush();
+ }
+
+ @Override
+ public void write(byte[] b) throws IOException {
+ write(b, 0, b.length);
+ }
+
+ @Override
+ public void write(byte[] b, int off, int len) throws IOException {
+ this.copy.write(b, off, len);
+ this.original.write(b, off, len);
+ }
+
+ @Override
+ public void flush() throws IOException {
+ this.copy.flush();
+ this.original.flush();
+ }
+
+ }
+
+ }
+
+}
diff --git a/lab/22-aop/src/test/java/rewards/DBExceptionHandlingAspectTests.java b/lab/22-aop/src/test/java/rewards/DBExceptionHandlingAspectTests.java
new file mode 100644
index 0000000..278e429
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/DBExceptionHandlingAspectTests.java
@@ -0,0 +1,48 @@
+package rewards;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import rewards.CaptureSystemOutput.OutputCapture;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.aspects.DBExceptionHandlingAspect;
+import rewards.internal.exception.RewardDataAccessException;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = { DbExceptionTestConfig.class })
+public class DBExceptionHandlingAspectTests {
+
+ @Autowired
+ AccountRepository repository;
+
+ @Test
+ @CaptureSystemOutput
+ public void testReportException(OutputCapture capture) {
+
+ // The repository.findByCreditCard(..) method below will
+ // result in an exception because we are using empty database
+ // set by DbExceptionTestConfig configuration class
+ // used by @ContextConfiguration annotation above.
+ assertThrows(RewardDataAccessException.class, () -> {
+ repository.findByCreditCard("1234123412341234");
+ });
+
+ // TODO-12: (Optional) Validate our AOP is working.
+ //
+ // - An error message should now be logged to the console as a warning
+ // - Save all your work and run this test - it should pass with a warning
+ // message on the console AND the console output assertion (below)
+ // should succeed.
+
+ if (TestConstants.CHECK_CONSOLE_OUTPUT) {
+ assertThat(capture.toString(), containsString(DBExceptionHandlingAspect.EMAIL_FAILURE_MSG));
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/test/java/rewards/DbExceptionTestConfig.java b/lab/22-aop/src/test/java/rewards/DbExceptionTestConfig.java
new file mode 100644
index 0000000..33b5afd
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/DbExceptionTestConfig.java
@@ -0,0 +1,31 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import config.AspectsConfig;
+import config.RewardsConfig;
+
+
+@Configuration
+@Import({RewardsConfig.class,AspectsConfig.class})
+public class DbExceptionTestConfig {
+
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder()).setName("rewards-dbexception")
+ // No scripts added. This will cause an exception.
+ .build();
+ }
+
+}
diff --git a/lab/22-aop/src/test/java/rewards/LoggingAspectTests.java b/lab/22-aop/src/test/java/rewards/LoggingAspectTests.java
new file mode 100644
index 0000000..9a278e0
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/LoggingAspectTests.java
@@ -0,0 +1,34 @@
+package rewards;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import rewards.CaptureSystemOutput.OutputCapture;
+import rewards.internal.account.AccountRepository;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes = { SystemTestConfig.class })
+public class LoggingAspectTests {
+
+ @Autowired
+ AccountRepository repository;
+
+ @Test
+ @CaptureSystemOutput
+ public void testLogger(OutputCapture capture) {
+ repository.findByCreditCard("1234123412341234");
+
+ if (TestConstants.CHECK_CONSOLE_OUTPUT) {
+ // AOP VERIFICATION
+ // LoggingAspect should have output an INFO message to console
+ String consoleOutput = capture.toString();
+ assertTrue(consoleOutput.startsWith("INFO"));
+ assertTrue(consoleOutput.contains("rewards.internal.aspects.LoggingAspect"));
+ }
+ }
+}
diff --git a/lab/22-aop/src/test/java/rewards/RewardNetworkTests.java b/lab/22-aop/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..c5ff5f8
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import rewards.CaptureSystemOutput.OutputCapture;
+import rewards.internal.aspects.LoggingAspect;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application
+ * work together to reward for dining successfully. Uses Spring to bootstrap the
+ * application for use in a test environment.
+ */
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration(classes={SystemTestConfig.class})
+public class RewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ @Test
+ @CaptureSystemOutput
+ public void testRewardForDining(OutputCapture capture) {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+
+ // TODO-06: Run this test. It should pass AND you should see TWO lines of
+ // log output from the LoggingAspect on the console
+ int expectedMatches = 2;
+ checkConsoleOutput(capture, expectedMatches);
+
+ // TODO-09: Save all your work, and change the expected matches value above from 2 to 4.
+ // Rerun the RewardNetworkTests. It should pass, and you should now see FOUR lines of
+ // console output from the LoggingAspect.
+ }
+
+ /**
+ * Not only must the code run, but the LoggingAspect should generate logging
+ * output to the console.
+ */
+ private void checkConsoleOutput(OutputCapture capture, int expectedMatches) {
+ // Don't run these checks until we are ready
+ if (!TestConstants.CHECK_CONSOLE_OUTPUT)
+ return;
+
+ // AOP VERIFICATION
+ // Expecting 4 lines of output from the LoggingAspect to console
+ String[] consoleOutput = capture.toString().split("\n");
+ int matches = 0;
+
+ for (String line : consoleOutput) {
+ if (line.contains("rewards.internal.aspects.LoggingAspect")) {
+ if (line.contains(LoggingAspect.BEFORE)) {
+ if (line.contains("JdbcAccountRepository") && line.contains("findByCreditCard"))
+ // Before aspect invoked for
+ // JdbcAccountRepository.findByCreditCard
+ matches++;
+ else if (line.contains("JdbcRestaurantRepository") && line.contains("findByMerchantNumber"))
+ // Before aspect invoked for
+ // JdbcRestaurantRepository.findByMerchantNumber
+ matches++;
+ } else if (line.contains(LoggingAspect.AROUND)) {
+ if (line.contains("AccountRepository") && line.contains("updateBeneficiaries"))
+ // Around aspect invoked for
+ // AccountRepository.updateBeneficiaries
+ matches++;
+ else if (line.contains("Around") && line.contains("RewardRepository")
+ && line.contains("updateReward"))
+ // Around aspect invoked for
+ // RewardRepository.updateReward
+ matches++;
+ }
+ }
+ }
+
+ assertEquals(expectedMatches, matches);
+ }
+
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/test/java/rewards/SystemTestConfig.java b/lab/22-aop/src/test/java/rewards/SystemTestConfig.java
new file mode 100644
index 0000000..ea44b48
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/SystemTestConfig.java
@@ -0,0 +1,36 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import config.RewardsConfig;
+
+
+/**
+ * TODO-05: Make this configuration include the aspect configuration.
+ * Save all your work, run the LoggingAspectTests. It should pass,
+ * and you should see one line of LoggingAspect output in the console.
+ */
+@Configuration
+@Import({RewardsConfig.class})
+public class SystemTestConfig {
+
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql")
+ .build();
+ }
+
+}
diff --git a/lab/22-aop/src/test/java/rewards/TestConstants.java b/lab/22-aop/src/test/java/rewards/TestConstants.java
new file mode 100644
index 0000000..c146c31
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/TestConstants.java
@@ -0,0 +1,14 @@
+package rewards;
+
+public class TestConstants {
+
+ // TODO-00: In this lab, you are going to exercise the following:
+ // - Creating aspect using Spring AOP
+ // - Writing pointcut expressions
+ // - Using various types of advices
+ //
+ // TODO-01: Enable checking of console output in our Tests.
+ // - Change the value below to true
+
+ public static final boolean CHECK_CONSOLE_OUTPUT = false;
+}
diff --git a/lab/22-aop/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/22-aop/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..98b7353
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,72 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/test/java/rewards/internal/StubAccountRepository.java b/lab/22-aop/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..b926be2
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,43 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/22-aop/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..418516d
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,53 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+import rewards.internal.restaurant.BenefitAvailabilityPolicy;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new AlwaysReturnsTrue());
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy that always returns true. Only useful for testing--a real
+ * availability policy might consider many factors such as the day of week of the dining, or the account's reward
+ * history for the current month.
+ */
+ private static class AlwaysReturnsTrue implements BenefitAvailabilityPolicy {
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/test/java/rewards/internal/StubRewardRepository.java b/lab/22-aop/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..d4871f8
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation updateReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/test/java/rewards/internal/account/AccountTests.java b/lab/22-aop/src/test/java/rewards/internal/account/AccountTests.java
new file mode 100644
index 0000000..4075654
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/internal/account/AccountTests.java
@@ -0,0 +1,57 @@
+package rewards.internal.account;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for the Account class that verify Account behavior works in isolation.
+ */
+public class AccountTests {
+
+ private final Account account = new Account("1", "Keith and Keri Donald");
+
+ @Test
+ public void accountIsValid() {
+ // setup account with a valid set of beneficiaries to prepare for testing
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ assertTrue(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWithNoBeneficiaries() {
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreOver100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreUnder100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("25%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void makeContribution() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ assertEquals(contribution.getAmount(), MonetaryAmount.valueOf("100.00"));
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java b/lab/22-aop/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
new file mode 100644
index 0000000..4326571
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
@@ -0,0 +1,96 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC account repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcAccountRepositoryTests {
+
+ private JdbcAccountRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ }
+
+ @Test
+ public void testFindAccountByCreditCard() {
+ Account account = repository.findByCreditCard("1234123412341234");
+ // assert the returned account contains what you expect given the state of the database
+ assertNotNull(account, "account should never be null");
+ assertEquals(Long.valueOf(0), account.getEntityId(), "wrong entity id");
+ assertEquals("123456789", account.getNumber(), "wrong account number");
+ assertEquals("Keith and Keri Donald", account.getName(), "wrong name");
+ assertEquals(2, account.getBeneficiaries().size(), "wrong beneficiary collection size");
+
+ Beneficiary b1 = account.getBeneficiary("Annabelle");
+ assertNotNull(b1, "Annabelle should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b1.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b1.getAllocationPercentage(), "wrong allocation percentage");
+
+ Beneficiary b2 = account.getBeneficiary("Corgan");
+ assertNotNull(b2, "Corgan should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b2.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b2.getAllocationPercentage(), "wrong allocation percentage");
+ }
+
+ @Test
+ public void testFindAccountByCreditCardNoAccount() {
+ assertThrows(EmptyResultDataAccessException.class, () -> {
+ repository.findByCreditCard("bogus");
+ });
+ }
+
+ @Test
+ public void testUpdateBeneficiaries() throws SQLException {
+ Account account = repository.findByCreditCard("1234123412341234");
+ account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ repository.updateBeneficiaries(account);
+ verifyBeneficiaryTableUpdated();
+ }
+
+ private void verifyBeneficiaryTableUpdated() throws SQLException {
+ String sql = "select SAVINGS from T_ACCOUNT_BENEFICIARY where NAME = ? and ACCOUNT_ID = ?";
+ PreparedStatement stmt = dataSource.getConnection().prepareStatement(sql);
+
+ // assert Annabelle has $4.00 savings now
+ stmt.setString(1, "Annabelle");
+ stmt.setLong(2, 0L);
+ ResultSet rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+
+ // assert Corgan has $4.00 savings now
+ stmt.setString(1, "Corgan");
+ stmt.setLong(2, 0L);
+ rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/22-aop/src/test/java/rewards/internal/aspects/RepositoryPerformanceLogTest.java b/lab/22-aop/src/test/java/rewards/internal/aspects/RepositoryPerformanceLogTest.java
new file mode 100644
index 0000000..bd4beab
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/internal/aspects/RepositoryPerformanceLogTest.java
@@ -0,0 +1,42 @@
+package rewards.internal.aspects;
+
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.easymock.EasyMock;
+import org.junit.jupiter.api.Test;
+
+import rewards.internal.monitor.jamon.JamonMonitorFactory;
+
+/**
+ * Unit test to test the behavior of the RepositoryPerformanceMonitor aspect in isolation.
+ */
+public class RepositoryPerformanceLogTest {
+
+ @Test
+ public void testMonitor() throws Throwable {
+ JamonMonitorFactory monitorFactory = new JamonMonitorFactory();
+ LoggingAspect performanceMonitor = new LoggingAspect(monitorFactory);
+ Signature signature = EasyMock.createMock(Signature.class);
+ ProceedingJoinPoint targetMethod = EasyMock.createMock(ProceedingJoinPoint.class);
+
+ expect(targetMethod.getSignature()).andReturn(signature);
+ expect(signature.getDeclaringType()).andReturn(Object.class);
+ expect(signature.getName()).andReturn("hashCode");
+ expect(targetMethod.proceed()).andReturn(new Object());
+
+ replay(signature, targetMethod);
+ performanceMonitor.monitor(targetMethod);
+
+ // This check only makes sense once we have configured LoggingAspect to
+ // be an @Aspect class (and marked monitor with @Around)
+ if (performanceMonitor.getClass().getAnnotation(Aspect.class) != null) {
+ verify(signature, targetMethod);
+ }
+ }
+
+}
diff --git a/lab/22-aop/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java b/lab/22-aop/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
new file mode 100644
index 0000000..bfe24ba
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
@@ -0,0 +1,52 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC restaurant repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRestaurantRepositoryTests {
+
+ private JdbcRestaurantRepository repository;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRestaurantRepository();
+ repository.setDataSource(createTestDataSource());
+ }
+
+ @Test
+ public void testFindRestaurantByMerchantNumber() {
+ Restaurant restaurant = repository.findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "the restaurant should never be null");
+ assertEquals("1234567890", restaurant.getNumber(), "the merchant number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "the name is wrong");
+ assertEquals(Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "the benefitPercentage is wrong");
+ assertEquals(JdbcRestaurantRepository.AlwaysAvailable.INSTANCE,
+ restaurant.getBenefitAvailabilityPolicy(), "the benefit availability policy is wrong");
+ }
+
+ @Test
+ public void testFindRestaurantByBogusMerchantNumber() {
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("bogus");
+ });
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/22-aop/src/test/java/rewards/internal/restaurant/RestaurantTests.java b/lab/22-aop/src/test/java/rewards/internal/restaurant/RestaurantTests.java
new file mode 100644
index 0000000..93c2496
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/internal/restaurant/RestaurantTests.java
@@ -0,0 +1,71 @@
+package rewards.internal.restaurant;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for exercising the behavior of the Restaurant aggregate entity. A restaurant calculates a benefit to award
+ * to an account for dining based on an availability policy and benefit percentage.
+ */
+public class RestaurantTests {
+
+ private Restaurant restaurant;
+
+ private Account account;
+
+ private Dining dining;
+
+ @BeforeEach
+ public void setUp() {
+ // configure the restaurant, the object being tested
+ restaurant = new Restaurant("1234567890", "AppleBee's");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(true));
+ // configure supporting objects needed by the restaurant
+ account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle");
+ dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+ }
+
+ @Test
+ public void testCalcuateBenefitFor() {
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert 8.00 eligible for reward
+ assertEquals(MonetaryAmount.valueOf("8.00"), benefit);
+ }
+
+ @Test
+ public void testNoBenefitAvailable() {
+ // configure stub that always returns false
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(false));
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert zero eligible for reward
+ assertEquals(MonetaryAmount.valueOf("0.00"), benefit);
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy containing a single flag used to determine if benefit is available.
+ * Only useful for testing--a real availability policy might consider many factors such as the day of week of the
+ * dining, or the account's reward history for the current month.
+ */
+ private static class StubBenefitAvailibilityPolicy implements BenefitAvailabilityPolicy {
+
+ private final boolean isBenefitAvailable;
+
+ public StubBenefitAvailibilityPolicy(boolean isBenefitAvailable) {
+ this.isBenefitAvailable = isBenefitAvailable;
+ }
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return isBenefitAvailable;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/22-aop/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java b/lab/22-aop/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
new file mode 100644
index 0000000..fbb71e5
--- /dev/null
+++ b/lab/22-aop/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
@@ -0,0 +1,80 @@
+package rewards.internal.reward;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.Account;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Tests the JDBC reward repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRewardRepositoryTests {
+
+ private JdbcRewardRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRewardRepository();
+ dataSource = createTestDataSource();
+ repository.setDataSource(dataSource);
+ }
+
+ @Test public void createReward() throws SQLException {
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "0123456789");
+
+ Account account = new Account("1", "Keith and Keri Donald");
+ account.setEntityId(0L);
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ RewardConfirmation confirmation = repository.updateReward(contribution, dining);
+ assertNotNull(confirmation, "confirmation should not be null");
+ assertNotNull(confirmation.getConfirmationNumber(), "confirmation number should not be null");
+ assertEquals(contribution, confirmation.getAccountContribution(), "wrong contribution object");
+ verifyRewardInserted(confirmation, dining);
+ }
+
+ private void verifyRewardInserted(RewardConfirmation confirmation, Dining dining) throws SQLException {
+ assertEquals(1, getRewardCount());
+ Statement stmt = dataSource.getConnection().createStatement();
+ ResultSet rs = stmt.executeQuery("select REWARD_AMOUNT from T_REWARD where CONFIRMATION_NUMBER = '"
+ + confirmation.getConfirmationNumber() + "'");
+ rs.next();
+ assertEquals(confirmation.getAccountContribution().getAmount(), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private int getRewardCount() throws SQLException {
+ Statement stmt = dataSource.getConnection().createStatement();
+ ResultSet rs = stmt.executeQuery("select count(*) from T_REWARD");
+ rs.next();
+ return rs.getInt(1);
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/24-test-solution/build.gradle b/lab/24-test-solution/build.gradle
new file mode 100644
index 0000000..af9f0d4
--- /dev/null
+++ b/lab/24-test-solution/build.gradle
@@ -0,0 +1,3 @@
+dependencies {
+ implementation project(':00-rewards-common')
+}
diff --git a/lab/24-test-solution/jndi/empty.file b/lab/24-test-solution/jndi/empty.file
new file mode 100644
index 0000000..e69de29
diff --git a/lab/24-test-solution/pom.xml b/lab/24-test-solution/pom.xml
new file mode 100644
index 0000000..ae66d08
--- /dev/null
+++ b/lab/24-test-solution/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+ 24-test-solution
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+ com.github.h-thurow
+ simple-jndi
+ 0.24.0
+ test
+
+
+
diff --git a/lab/24-test-solution/src/main/java/config/RewardsConfig.java b/lab/24-test-solution/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..00cd3e0
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,10 @@
+package config;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ComponentScan("rewards")
+public class RewardsConfig {
+
+}
diff --git a/lab/24-test-solution/src/main/java/rewards/AccountContribution.java b/lab/24-test-solution/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/main/java/rewards/Dining.java b/lab/24-test-solution/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/main/java/rewards/RewardConfirmation.java b/lab/24-test-solution/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/main/java/rewards/RewardNetwork.java b/lab/24-test-solution/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/24-test-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..d07b40a
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,55 @@
+package rewards.internal;
+
+import org.springframework.stereotype.Service;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+@Service("rewardNetwork")
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/main/java/rewards/internal/account/Account.java b/lab/24-test-solution/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..51119d4
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,156 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public Beneficiary getBeneficiary(String beneficiaryName) {
+ for (Beneficiary b : beneficiaries) {
+ if (b.getName().equals(beneficiaryName))
+ return b;
+ }
+
+ return null;
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ // we are not using the Percentage class here because
+ double totalPercentage = 0;
+ for (Beneficiary b : beneficiaries) {
+ totalPercentage += b.getAllocationPercentage().asDouble();
+ }
+ return totalPercentage == 1.0;
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account.
+ *
+ * Callers should not attempt to hold on or modify the returned set. This method should only be used transitively;
+ * for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/main/java/rewards/internal/account/AccountRepository.java b/lab/24-test-solution/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/main/java/rewards/internal/account/Beneficiary.java b/lab/24-test-solution/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/24-test-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..7e4c4ee
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,153 @@
+package rewards.internal.account;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Profile;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.stereotype.Repository;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+@Profile("jdbc")
+@Repository
+public class JdbcAccountRepository implements AccountRepository {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private DataSource dataSource;
+
+ /**
+ * Constructor logs creation so we know which repository we are using.
+ */
+ public JdbcAccountRepository() {
+ logger.info("Creating " + getClass().getSimpleName());
+ }
+
+ /**
+ * Sets the data source this repository will use to load accounts.
+ *
+ * @param dataSource
+ * the data source
+ */
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = "select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?";
+ Account account = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, creditCardNumber);
+ rs = ps.executeQuery();
+ account = mapAccount(rs);
+ } catch (SQLException e) {
+ throw new RuntimeException(
+ "SQL exception occurred finding by credit card number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ for (Beneficiary beneficiary : account.getBeneficiaries()) {
+ ps.setBigDecimal(1, beneficiary.getSavings().asBigDecimal());
+ ps.setLong(2, account.getEntityId());
+ ps.setString(3, beneficiary.getName());
+ ps.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException(
+ "SQL exception occurred updating beneficiary savings", e);
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and
+ * T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account aggregate.
+ *
+ * @param rs
+ * the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException
+ * an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary
+ * object.
+ *
+ * @param rs
+ * the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException
+ * an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs
+ .getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs
+ .getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/main/java/rewards/internal/account/package.html b/lab/24-test-solution/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/24-test-solution/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/24-test-solution/src/test/java/rewards/DevRewardNetworkTests.java b/lab/24-test-solution/src/test/java/rewards/DevRewardNetworkTests.java
new file mode 100644
index 0000000..f56a023
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/DevRewardNetworkTests.java
@@ -0,0 +1,62 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application
+ * work together to reward for dining successfully. Uses Spring to bootstrap the
+ * application for use in a test environment using the development in-memory
+ * database and JDBC implementations of the repositories.
+ */
+@SpringJUnitConfig(classes=TestInfrastructureConfig.class)
+@ActiveProfiles({ "local", "jdbc" })
+public class DevRewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ @Test
+ @DisplayName("test if reward computation and distribution works")
+ public void rewardForDining() {
+ // create a new dining of 100.00 charged to credit card
+ // '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234",
+ "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork
+ .rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation
+ .getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertAll("distribution of reward",
+ () -> assertEquals(2, contribution.getDistributions().size()),
+ () -> assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount()),
+ () -> assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount()));
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/test/java/rewards/LoggingBeanPostProcessor.java b/lab/24-test-solution/src/test/java/rewards/LoggingBeanPostProcessor.java
new file mode 100644
index 0000000..d084def
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/LoggingBeanPostProcessor.java
@@ -0,0 +1,35 @@
+package rewards;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+
+/**
+ * A simple bean post-processor that logs each newly created bean it sees. Inner
+ * beans are ignored. A very easy way to see what beans you have created and
+ * less verbose than turning on Spring's internal logging.
+ */
+public class LoggingBeanPostProcessor implements BeanPostProcessor {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Override
+ public Object postProcessBeforeInitialization(Object bean, String beanName)
+ throws BeansException {
+ return bean;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName)
+ throws BeansException {
+
+ // Log the names and types of all non inner-beans created
+ if (!beanName.contains("inner bean"))
+ logger.info("NEW " + bean.getClass().getSimpleName() + " -> "
+ + beanName);
+
+ return bean;
+ }
+
+}
diff --git a/lab/24-test-solution/src/test/java/rewards/ProductionRewardNetworkTests.java b/lab/24-test-solution/src/test/java/rewards/ProductionRewardNetworkTests.java
new file mode 100644
index 0000000..673ef44
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/ProductionRewardNetworkTests.java
@@ -0,0 +1,63 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application
+ * work together to reward for dining successfully. Uses Spring to bootstrap the
+ * application for use in a test environment using a JNDI look-up foe the
+ * dataSource (as would be used in production) and JDBC implementations of the
+ * repositories.
+ */
+@SpringJUnitConfig(classes=TestInfrastructureConfig.class)
+@ActiveProfiles({ "jndi", "jdbc" })
+public class ProductionRewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ @Test
+ @DisplayName("test if reward computation and distribution works")
+ public void rewardForDining() {
+ // create a new dining of 100.00 charged to credit card
+ // '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234",
+ "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork
+ .rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation
+ .getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertAll("distribution of reward",
+ () -> assertEquals(2, contribution.getDistributions().size()),
+ () -> assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount()),
+ () -> assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount()));
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/test/java/rewards/RewardNetworkTests.java b/lab/24-test-solution/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..2387c76
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,65 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application
+ * work together to reward for dining successfully. Uses Spring to bootstrap the
+ * application for use in a test environment.
+ */
+@SpringJUnitConfig(classes=TestInfrastructureConfig.class)
+
+// Uncomment the profile you wish to use
+@ActiveProfiles("stub")
+//@ActiveProfiles({ "local", "jdbc" })
+//@ActiveProfiles({ "jndi", "jdbc" })
+public class RewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ @Test
+ @DisplayName("test if reward computation and distribution works")
+ public void rewardForDining() {
+ // create a new dining of 100.00 charged to credit card
+ // '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234",
+ "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork
+ .rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation
+ .getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertAll("distribution of reward",
+ () -> assertEquals(2, contribution.getDistributions().size()),
+ () -> assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount()),
+ () -> assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount()));
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/test/java/rewards/SimpleJndiHelper.java b/lab/24-test-solution/src/test/java/rewards/SimpleJndiHelper.java
new file mode 100644
index 0000000..ba421c1
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/SimpleJndiHelper.java
@@ -0,0 +1,72 @@
+package rewards;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+
+/**
+ * This class sets up Simple JNDI, creates an embedded dataSource and registers
+ * it with JNDI. Normally JNDI is provided by a container such as Tomcat. This
+ * allows JNDI to be used standalone in a test.
+ *
+ * We need this to be registered and available before any Spring beans are
+ * created, or our JNDI lookup will fail. We have therefore made this bean into
+ * a BeanFactoryPostProcessor - it will be invoked just before the
+ * first bean is created.
+ */
+public class SimpleJndiHelper implements BeanFactoryPostProcessor {
+
+ public static final String REWARDS_DB_JNDI_PATH = "java:/comp/env/jdbc/rewards";
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ public void doJndiSetup() {
+ System.setProperty(Context.INITIAL_CONTEXT_FACTORY, //
+ "org.osjava.sj.SimpleContextFactory");
+ System.setProperty("org.osjava.sj.root", "jndi");
+ System.setProperty("org.osjava.jndi.delimiter", "/");
+ System.setProperty("org.osjava.sj.jndi.shared", "true");
+
+ logger.info("Running JDNI setup");
+
+ try {
+ InitialContext ic = new InitialContext();
+
+ // Construct DataSource
+ DataSource ds = new EmbeddedDatabaseBuilder() //
+ .addScript("classpath:rewards/testdb/schema.sql") //
+ .addScript("classpath:rewards/testdb/data.sql") //
+ .build();
+
+ // Bind as a JNDI resource
+ ic.rebind(REWARDS_DB_JNDI_PATH, ds);
+ logger.info("JNDI Resource '" + REWARDS_DB_JNDI_PATH //
+ + "' instanceof " + ds.getClass().getSimpleName());
+ } catch (NamingException ex) {
+ logger.error("JNDI setup error", ex);
+ ex.printStackTrace();
+ System.exit(0);
+ }
+
+ logger.info("JNDI Registrations completed.");
+ }
+
+ /**
+ * Using the BeanFactoryPostProcessor as a convenient entry-point to do setup
+ * before Spring creates any brans.
+ */
+ @Override
+ public void postProcessBeanFactory( //
+ ConfigurableListableBeanFactory beanFactory) throws BeansException {
+ doJndiSetup();
+ return;
+ }
+
+}
diff --git a/lab/24-test-solution/src/test/java/rewards/StubRewardNetworkTests.java b/lab/24-test-solution/src/test/java/rewards/StubRewardNetworkTests.java
new file mode 100644
index 0000000..3eb2403
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/StubRewardNetworkTests.java
@@ -0,0 +1,63 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application
+ * work together to reward for dining successfully. Uses Spring to bootstrap the
+ * application for use in a test environment using Stub repository classes.
+ */
+@SpringJUnitConfig(classes=TestInfrastructureConfig.class)
+@ActiveProfiles("stub")
+public class StubRewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ @Test
+ public void rewardForDining() {
+ // create a new dining of 100.00 charged to credit card
+ // '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234",
+ "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork
+ .rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation
+ .getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2
+ // distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution
+ .getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution
+ .getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/test/java/rewards/TestInfrastructureConfig.java b/lab/24-test-solution/src/test/java/rewards/TestInfrastructureConfig.java
new file mode 100644
index 0000000..371cfde
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/TestInfrastructureConfig.java
@@ -0,0 +1,23 @@
+package rewards;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import config.RewardsConfig;
+
+@Configuration
+@Import({
+ TestInfrastructureLocalConfig.class,
+ TestInfrastructureJndiConfig.class,
+ RewardsConfig.class })
+public class TestInfrastructureConfig {
+
+ /**
+ * The bean logging post-processor from the bean lifecycle slides.
+ */
+ @Bean
+ public static LoggingBeanPostProcessor loggingBean(){
+ return new LoggingBeanPostProcessor();
+ }
+}
diff --git a/lab/24-test-solution/src/test/java/rewards/TestInfrastructureJndiConfig.java b/lab/24-test-solution/src/test/java/rewards/TestInfrastructureJndiConfig.java
new file mode 100644
index 0000000..465ec8d
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/TestInfrastructureJndiConfig.java
@@ -0,0 +1,28 @@
+package rewards;
+
+import javax.naming.InitialContext;
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+
+@Configuration
+@Profile("jndi")
+public class TestInfrastructureJndiConfig {
+
+ /**
+ * Static method because we are defining a Bean post-processor.
+ */
+ @Bean
+ public static SimpleJndiHelper jndiHelper(){
+ return new SimpleJndiHelper();
+ }
+
+ @Bean
+ public DataSource dataSource() throws Exception {
+ return (DataSource)
+ (new InitialContext())
+ .lookup("java:/comp/env/jdbc/rewards");
+ }
+}
diff --git a/lab/24-test-solution/src/test/java/rewards/TestInfrastructureLocalConfig.java b/lab/24-test-solution/src/test/java/rewards/TestInfrastructureLocalConfig.java
new file mode 100644
index 0000000..f8f63d6
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/TestInfrastructureLocalConfig.java
@@ -0,0 +1,26 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+@Configuration
+@Profile("local")
+public class TestInfrastructureLocalConfig {
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/24-test-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/24-test-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..98b7353
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,72 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/test/java/rewards/internal/StubAccountRepository.java b/lab/24-test-solution/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..ae91e4d
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,58 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Profile;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.stereotype.Repository;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account
+ * "Keith and Keri Donald" with two beneficiaries "Annabelle" (50% allocation)
+ * and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can
+ * work with this stub and not have to bring in expensive and/or complex
+ * dependencies such as a Database. Simple unit tests can then verify object
+ * behavior by considering the state of this stub.
+ */
+@Profile("stub")
+@Repository("accountRepository")
+public class StubAccountRepository implements AccountRepository {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ /**
+ * Creates a single test account with two beneficiaries. Also logs creation
+ * so we know which repository we are using.
+ */
+ public StubAccountRepository() {
+ logger.info("Creating " + getClass().getSimpleName());
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/24-test-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..04873a6
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,53 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Profile;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.stereotype.Repository;
+
+import common.money.Percentage;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant
+ * "Apple Bees" with a 8% benefit availability percentage that's always
+ * available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can
+ * work with this stub and not have to bring in expensive and/or complex
+ * dependencies such as a Database. Simple unit tests can then verify object
+ * behavior by considering the state of this stub.
+ */
+@Profile("stub")
+@Repository("restaurantRepository")
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ /**
+ * Creates a single test restaurant with an 8% benefit policy. Also logs
+ * creation so we know which repository we are using.
+ */
+ public StubRestaurantRepository() {
+ logger.info("Creating " + getClass().getSimpleName());
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber
+ .get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/test/java/rewards/internal/StubRewardRepository.java b/lab/24-test-solution/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..eb51568
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,39 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Profile;
+import org.springframework.stereotype.Repository;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+@Profile("stub")
+@Repository("rewardRepository")
+public class StubRewardRepository implements RewardRepository {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ /**
+ * Constructor logs creation so we know which repository we are using.
+ */
+ public StubRewardRepository() {
+ logger.info("Creating " + getClass().getSimpleName());
+ }
+
+ public RewardConfirmation confirmReward(AccountContribution contribution,
+ Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/test/java/rewards/internal/account/AccountTests.java b/lab/24-test-solution/src/test/java/rewards/internal/account/AccountTests.java
new file mode 100644
index 0000000..be9a315
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/internal/account/AccountTests.java
@@ -0,0 +1,69 @@
+package rewards.internal.account;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for the Account class that verify Account behavior works in isolation.
+ */
+public class AccountTests {
+
+ private final Account account = new Account("1", "Keith and Keri Donald");
+
+ @Test
+ public void accountIsValid() {
+ // setup account with a valid set of beneficiaries to prepare for testing
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ assertTrue(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWithNoBeneficiaries() {
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreOver100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreUnder100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("25%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void makeContribution() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ assertEquals(contribution.getAmount(), MonetaryAmount.valueOf("100.00"));
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+
+ @Test
+ public void throwIllegalStateExceptionWhenContributionIsInvalid() {
+ Throwable exception = assertThrows(IllegalStateException.class,
+ () -> {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ });
+ assertEquals("Cannot make contributions to this account: it has invalid beneficiary allocations", exception.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java b/lab/24-test-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
new file mode 100644
index 0000000..4326571
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
@@ -0,0 +1,96 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC account repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcAccountRepositoryTests {
+
+ private JdbcAccountRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ }
+
+ @Test
+ public void testFindAccountByCreditCard() {
+ Account account = repository.findByCreditCard("1234123412341234");
+ // assert the returned account contains what you expect given the state of the database
+ assertNotNull(account, "account should never be null");
+ assertEquals(Long.valueOf(0), account.getEntityId(), "wrong entity id");
+ assertEquals("123456789", account.getNumber(), "wrong account number");
+ assertEquals("Keith and Keri Donald", account.getName(), "wrong name");
+ assertEquals(2, account.getBeneficiaries().size(), "wrong beneficiary collection size");
+
+ Beneficiary b1 = account.getBeneficiary("Annabelle");
+ assertNotNull(b1, "Annabelle should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b1.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b1.getAllocationPercentage(), "wrong allocation percentage");
+
+ Beneficiary b2 = account.getBeneficiary("Corgan");
+ assertNotNull(b2, "Corgan should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b2.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b2.getAllocationPercentage(), "wrong allocation percentage");
+ }
+
+ @Test
+ public void testFindAccountByCreditCardNoAccount() {
+ assertThrows(EmptyResultDataAccessException.class, () -> {
+ repository.findByCreditCard("bogus");
+ });
+ }
+
+ @Test
+ public void testUpdateBeneficiaries() throws SQLException {
+ Account account = repository.findByCreditCard("1234123412341234");
+ account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ repository.updateBeneficiaries(account);
+ verifyBeneficiaryTableUpdated();
+ }
+
+ private void verifyBeneficiaryTableUpdated() throws SQLException {
+ String sql = "select SAVINGS from T_ACCOUNT_BENEFICIARY where NAME = ? and ACCOUNT_ID = ?";
+ PreparedStatement stmt = dataSource.getConnection().prepareStatement(sql);
+
+ // assert Annabelle has $4.00 savings now
+ stmt.setString(1, "Annabelle");
+ stmt.setLong(2, 0L);
+ ResultSet rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+
+ // assert Corgan has $4.00 savings now
+ stmt.setString(1, "Corgan");
+ stmt.setLong(2, 0L);
+ rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/24-test-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java b/lab/24-test-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
new file mode 100644
index 0000000..f390d72
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
@@ -0,0 +1,78 @@
+package rewards.internal.restaurant;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import common.money.Percentage;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC restaurant repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRestaurantRepositoryTests {
+
+ private JdbcRestaurantRepository repository;
+
+ @BeforeEach
+ public void setUp() {
+ // simulate the Spring bean initialization lifecycle:
+
+ // first, construct the bean
+ repository = new JdbcRestaurantRepository();
+
+ // then, inject its dependencies
+ repository.setDataSource(createTestDataSource());
+
+ // lastly, initialize the bean
+ repository.populateRestaurantCache();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // simulate the Spring bean destruction lifecycle:
+
+ // destroy the bean
+ repository.clearRestaurantCache();
+ }
+
+ @Test
+ public void findRestaurantByMerchantNumber() {
+ Restaurant restaurant = repository.findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "the restaurant should never be null");
+ assertEquals("1234567890", restaurant.getNumber(), "the merchant number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "the name is wrong");
+ assertEquals(Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "the benefitPercentage is wrong");
+ }
+
+ @Test
+ public void testFindRestaurantByBogusMerchantNumber() {
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("bogus");
+ });
+ }
+
+ @Test
+ public void restaurantCacheClearedAfterDestroy() {
+ // force early tear down
+ tearDown();
+
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("1234567890");
+ });
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/24-test-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java b/lab/24-test-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
new file mode 100644
index 0000000..b366df4
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
@@ -0,0 +1,81 @@
+package rewards.internal.reward;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.Account;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Tests the JDBC reward repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRewardRepositoryTests {
+
+ private JdbcRewardRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRewardRepository();
+ dataSource = createTestDataSource();
+ repository.setDataSource(dataSource);
+ }
+
+ @Test
+ public void testCreateReward() throws SQLException {
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "0123456789");
+
+ Account account = new Account("1", "Keith and Keri Donald");
+ account.setEntityId(0L);
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ RewardConfirmation confirmation = repository.confirmReward(contribution, dining);
+ assertNotNull(confirmation, "confirmation should not be null");
+ assertNotNull(confirmation.getConfirmationNumber(), "confirmation number should not be null");
+ assertEquals(contribution, confirmation.getAccountContribution(), "wrong contribution object");
+ verifyRewardInserted(confirmation, dining);
+ }
+
+ private void verifyRewardInserted(RewardConfirmation confirmation, Dining dining) throws SQLException {
+ assertEquals(1, getRewardCount());
+ Statement stmt = dataSource.getConnection().createStatement();
+ ResultSet rs = stmt.executeQuery("select REWARD_AMOUNT from T_REWARD where CONFIRMATION_NUMBER = '"
+ + confirmation.getConfirmationNumber() + "'");
+ rs.next();
+ assertEquals(confirmation.getAccountContribution().getAmount(), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private int getRewardCount() throws SQLException {
+ Statement stmt = dataSource.getConnection().createStatement();
+ ResultSet rs = stmt.executeQuery("select count(*) from T_REWARD");
+ rs.next();
+ return rs.getInt(1);
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/24-test-solution/src/test/java/rewards/jndi.properties b/lab/24-test-solution/src/test/java/rewards/jndi.properties
new file mode 100644
index 0000000..3bfd943
--- /dev/null
+++ b/lab/24-test-solution/src/test/java/rewards/jndi.properties
@@ -0,0 +1,4 @@
+java.naming.factory.initial=org.osjava.sj.SimpleContextFactory
+org.osjava.sj.root=target/test-classes/config
+org.osjava.jndi.delimiter=/
+org.osjava.sj.jndi.shared=true
\ No newline at end of file
diff --git a/lab/24-test-solution/src/test/resources/jndi/jndi.properties b/lab/24-test-solution/src/test/resources/jndi/jndi.properties
new file mode 100644
index 0000000..3bfd943
--- /dev/null
+++ b/lab/24-test-solution/src/test/resources/jndi/jndi.properties
@@ -0,0 +1,4 @@
+java.naming.factory.initial=org.osjava.sj.SimpleContextFactory
+org.osjava.sj.root=target/test-classes/config
+org.osjava.jndi.delimiter=/
+org.osjava.sj.jndi.shared=true
\ No newline at end of file
diff --git a/lab/24-test/build.gradle b/lab/24-test/build.gradle
new file mode 100644
index 0000000..af9f0d4
--- /dev/null
+++ b/lab/24-test/build.gradle
@@ -0,0 +1,3 @@
+dependencies {
+ implementation project(':00-rewards-common')
+}
diff --git a/lab/24-test/jndi/empty.file b/lab/24-test/jndi/empty.file
new file mode 100644
index 0000000..e69de29
diff --git a/lab/24-test/pom.xml b/lab/24-test/pom.xml
new file mode 100644
index 0000000..60c0a21
--- /dev/null
+++ b/lab/24-test/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+ 24-test
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+ com.github.h-thurow
+ simple-jndi
+ 0.24.0
+ test
+
+
+
diff --git a/lab/24-test/src/main/java/config/RewardsConfig.java b/lab/24-test/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..00cd3e0
--- /dev/null
+++ b/lab/24-test/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,10 @@
+package config;
+
+import org.springframework.context.annotation.ComponentScan;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+@ComponentScan("rewards")
+public class RewardsConfig {
+
+}
diff --git a/lab/24-test/src/main/java/rewards/AccountContribution.java b/lab/24-test/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/Dining.java b/lab/24-test/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/RewardConfirmation.java b/lab/24-test/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/RewardNetwork.java b/lab/24-test/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/24-test/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..d07b40a
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,55 @@
+package rewards.internal;
+
+import org.springframework.stereotype.Service;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+@Service("rewardNetwork")
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/internal/account/Account.java b/lab/24-test/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..84463f0
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,159 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ try {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ } catch (IllegalArgumentException e) {
+ // total would have been over 100% - return invalid
+ return false;
+ }
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account. Callers should not attempt to hold on or modify the returned set.
+ * This method should only be used transitively; for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Returns a single account beneficiary. Callers should not attempt to hold on or modify the returned object. This
+ * method should only be used transitively; for example, called to facilitate reporting or testing.
+ * @param name the name of the beneficiary e.g "Annabelle"
+ * @return the beneficiary object
+ */
+ public Beneficiary getBeneficiary(String name) {
+ for (Beneficiary b : beneficiaries) {
+ if (b.getName().equals(name)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("No such beneficiary with name '" + name + "'");
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/internal/account/AccountRepository.java b/lab/24-test/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/internal/account/Beneficiary.java b/lab/24-test/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/24-test/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..c934040
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,138 @@
+package rewards.internal.account;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.stereotype.Repository;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+@Repository
+public class JdbcAccountRepository implements AccountRepository {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private DataSource dataSource;
+
+ /**
+ * Constructor logs creation so we know which repository we are using.
+ */
+ public JdbcAccountRepository() {
+ logger.info("Creating " + getClass().getSimpleName());
+ }
+
+ /**
+ * Sets the data source this repository will use to load accounts.
+ * @param dataSource the data source
+ */
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = "select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?";
+ Account account = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, creditCardNumber);
+ rs = ps.executeQuery();
+ account = mapAccount(rs);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred finding by credit card number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ for (Beneficiary beneficiary : account.getBeneficiaries()) {
+ ps.setBigDecimal(1, beneficiary.getSavings().asBigDecimal());
+ ps.setLong(2, account.getEntityId());
+ ps.setString(3, beneficiary.getName());
+ ps.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred updating beneficiary savings", e);
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/internal/account/package.html b/lab/24-test/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/lab/24-test/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/24-test/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..fab68fa
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,120 @@
+package rewards.internal.restaurant;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.sql.DataSource;
+import jakarta.annotation.PostConstruct;
+import jakarta.annotation.PreDestroy;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.stereotype.Repository;
+
+import common.money.Percentage;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ */
+@Repository
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private DataSource dataSource;
+
+ /**
+ * The Restaurant object cache. Cached restaurants are indexed by their merchant numbers.
+ */
+ private Map restaurantCache;
+
+ /**
+ * Constructor logs creation so we know which repository we are using.
+ */
+ public JdbcRestaurantRepository() {
+ logger.info("Creating " + getClass().getSimpleName());
+ }
+
+ /**
+ * Sets the data source this repository will use to load restaurants.
+ *
+ * @param dataSource the data source
+ */
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ return queryRestaurantCache(merchantNumber);
+ }
+
+ /**
+ * Helper method that populates the {@link #restaurantCache restaurant object cache} from rows in the T_RESTAURANT
+ * table. Cached restaurants are indexed by their merchant numbers. This method is called on initialization.
+ */
+ @PostConstruct
+ void populateRestaurantCache() {
+ logger.info("Loading restaurant cache");
+ restaurantCache = new HashMap<>();
+ String sql = "select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE from T_RESTAURANT";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ResultSet rs = ps.executeQuery()) {
+ while (rs.next()) {
+ Restaurant restaurant = mapRestaurant(rs);
+ // index the restaurant by its merchant number
+ restaurantCache.put(restaurant.getNumber(), restaurant);
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred finding by merchant number", e);
+ }
+
+ logger.info("Finished loading restaurant cache");
+ }
+
+ /**
+ * Helper method that simply queries the cache of restaurants.
+ *
+ * @param merchantNumber the restaurant's merchant number
+ * @return the restaurant
+ * @throws EmptyResultDataAccessException if no restaurant was found with that merchant number
+ */
+ private Restaurant queryRestaurantCache(String merchantNumber) {
+ Restaurant restaurant = restaurantCache.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+
+ /**
+ * Helper method that clears the cache of restaurants. This method is called on destruction
+ */
+ @PreDestroy
+ void clearRestaurantCache() {
+ logger.info("Clearing restaurant cache");
+ restaurantCache.clear();
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+ // map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/24-test/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..230890c
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,80 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on an availability policy and a
+ * benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/24-test/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/24-test/src/main/java/rewards/internal/restaurant/package.html b/lab/24-test/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/24-test/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/24-test/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..b71a84d
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,72 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+import javax.sql.DataSource;
+import java.sql.*;
+
+/**
+ * JDBC implementation of a reward repository that records the result of a reward transaction by inserting a reward
+ * confirmation record.
+ */
+@Repository
+public class JdbcRewardRepository implements RewardRepository {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private DataSource dataSource;
+
+ /**
+ * Constructor logs creation so we know which repository we are using.
+ */
+ public JdbcRewardRepository() {
+ logger.info("Creating " + getClass().getSimpleName());
+ }
+
+ /**
+ * Sets the data source this repository will use to insert rewards.
+ * @param dataSource the data source
+ */
+ @Autowired
+ public void setDataSource(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ String confirmationNumber = nextConfirmationNumber();
+ ps.setString(1, confirmationNumber);
+ ps.setBigDecimal(2, contribution.getAmount().asBigDecimal());
+ ps.setDate(3, new Date(SimpleDate.today().inMilliseconds()));
+ ps.setString(4, contribution.getAccountNumber());
+ ps.setString(5, dining.getMerchantNumber());
+ ps.setDate(6, new Date(dining.getDate().inMilliseconds()));
+ ps.setBigDecimal(7, dining.getAmount().asBigDecimal());
+ ps.execute();
+ return new RewardConfirmation(confirmationNumber, contribution);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred inserting reward record", e);
+ }
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ResultSet rs = ps.executeQuery()) {
+ rs.next();
+ return rs.getString(1);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception getting next confirmation number", e);
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/24-test/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..1207f0f
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/24-test/src/main/java/rewards/internal/reward/package.html b/lab/24-test/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/24-test/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/24-test/src/test/java/rewards/LoggingBeanPostProcessor.java b/lab/24-test/src/test/java/rewards/LoggingBeanPostProcessor.java
new file mode 100644
index 0000000..d084def
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/LoggingBeanPostProcessor.java
@@ -0,0 +1,35 @@
+package rewards;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanPostProcessor;
+
+/**
+ * A simple bean post-processor that logs each newly created bean it sees. Inner
+ * beans are ignored. A very easy way to see what beans you have created and
+ * less verbose than turning on Spring's internal logging.
+ */
+public class LoggingBeanPostProcessor implements BeanPostProcessor {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ @Override
+ public Object postProcessBeforeInitialization(Object bean, String beanName)
+ throws BeansException {
+ return bean;
+ }
+
+ @Override
+ public Object postProcessAfterInitialization(Object bean, String beanName)
+ throws BeansException {
+
+ // Log the names and types of all non inner-beans created
+ if (!beanName.contains("inner bean"))
+ logger.info("NEW " + bean.getClass().getSimpleName() + " -> "
+ + beanName);
+
+ return bean;
+ }
+
+}
diff --git a/lab/24-test/src/test/java/rewards/RewardNetworkTests.java b/lab/24-test/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..becedeb
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,153 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ConfigurableApplicationContext;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application
+ * work together to reward for dining successfully. Uses Spring to bootstrap the
+ * application for use in a test environment.
+ */
+
+/*
+ * TODO-00: In this lab, you are going to exercise the following:
+ * - Using annotation(s) from Spring TestContext Framework for
+ * creating application context for the test
+ * - Using profiles in the test
+ *
+ * TODO-01: Use Spring TestContext Framework
+ * - Read through Spring document on Spring TestContext Framework
+ * (https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/testing.html#testcontext-framework)
+ * - Add annotation(s) to this class so that it can
+ * use Spring TestContext Framework
+ * - Remove setUp() and tearDown() methods
+ * - Remove the attribute "context" which is not needed anymore.
+ * - Run the current test. Observe a test failure.
+ * - Use @Autowired to populate the rewardNetwork bean.
+ * - Re-run the current test, it should pass.
+ */
+
+/* TODO-02: Annotate all 'Stub*Repository' classes with @Repository
+ * - In the package rewards/internal, annotate all 'Stub*Repository' classes
+ * with the @Repository annotation (WITHOUT specifying any profile yet).
+ * (Make sure you are changing code in the '24-test' project.)
+ * - Rerun the current test, it should fail. Why?
+ */
+
+/* TODO-03: Assign the 'jdbc' profile to all Jdbc*Repository classes
+ * - Using the @Profile annotation, assign the 'jdbc' profile to all Jdbc*Repository classes
+ * (such as JdbcAccountRepository). (Be sure to annotate the actual repository classes in
+ * src/main/java, not the test classes in src/main/test!)
+ * - In the same way, assign the 'stub' profile to all Stub*Repository classes
+ * (such as StubAccountRepository)
+ * - Add @ActiveProfiles to this test class (below) and specify the "stub" profile.
+ * - Run the current test, it should pass.
+ * - Examine the logs, they should indicate "stub" repositories were used.
+ */
+
+/* TODO-04: Change active-profile to "jdbc".
+ * - Rerun the test, it should pass.
+ * - Check which repository implementations are being used now.
+ */
+
+/* TODO-05: Assign beans to the "local" profile
+ * - Go to corresponding step in TestInfrastructureLocalConfig class.
+ */
+
+/* TODO-06: Use "jdbc" and "local" as active profiles
+ * - Now that the bean 'dataSource' is specific to the local profile, should we expect
+ * this test to be successful?
+ * - Make the appropriate changes so the current test uses 2 profiles ('jdbc' and 'local').
+ * - Rerun the test, it should pass.
+ */
+
+/* TODO-07: Use "jdbc" and "jndi" as active profiles
+ * - Open TestInfrastructureJndiConfig and note the different datasource that will be
+ * used if the profile = 'jndi'.
+ * - Now update the current test so it uses profiles 'jdbc' and 'jndi'.
+ * - Rerun the test, it should pass.
+ */
+
+/* TODO-08 (Optional): Create an inner static class from TestInfrastructureConfig
+ * - Once inner static class is created, remove configuration
+ * class reference to TestInfrastructureConfig class from the annotation
+ * you added to this class in TO DO-01 above. (For more detailed on, refer tp
+ * lab document.)
+ * - Run the test again.
+ */
+
+public class RewardNetworkTests {
+
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetwork rewardNetwork;
+
+ /**
+ * Need this to enable clean shutdown at the end of the application
+ */
+ private ConfigurableApplicationContext context;
+
+ @BeforeEach
+ public void setUp() {
+ // Create the test configuration for the application from one file
+ context = SpringApplication.run(TestInfrastructureConfig.class);
+ // Get the bean to use to invoke the application
+ rewardNetwork = context.getBean(RewardNetwork.class);
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // simulate the Spring bean destruction lifecycle:
+ if (context != null)
+ context.close();
+ }
+
+ @Test
+ @DisplayName("Test if reward computation and distribution works")
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card
+ // '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234",
+ "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork
+ .rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation
+ .getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2
+ // distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // The total contribution amount should have been split into 2 distributions
+ // each distribution should be 4.00 (as both have a 50% allocation).
+ // The assertAll() is from JUnit 5 to group related checks together.
+ assertAll("distribution of reward",
+ () -> assertEquals(2, contribution.getDistributions().size()),
+ () -> assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount()),
+ () -> assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount()));
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/test/java/rewards/SimpleJndiHelper.java b/lab/24-test/src/test/java/rewards/SimpleJndiHelper.java
new file mode 100644
index 0000000..384da61
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/SimpleJndiHelper.java
@@ -0,0 +1,68 @@
+package rewards;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.naming.Context;
+import javax.naming.InitialContext;
+import javax.naming.NamingException;
+import javax.sql.DataSource;
+
+/**
+ * This class sets up Simple JNDI, creates an embedded dataSource and registers
+ * it with JNDI. Normally JNDI is provided by a container such as Tomcat. This
+ * allows JNDI to be used standalone in a test.
+ *
+ * We need this to be registered and available before any Spring beans are
+ * created, or our JNDI lookup will fail. We have therefore made this bean into
+ * a BeanFactoryPostProcessor - it will be invoked just before the
+ * first bean is created.
+ */
+public class SimpleJndiHelper implements BeanFactoryPostProcessor {
+
+ public static final String REWARDS_DB_JNDI_PATH = "java:/comp/env/jdbc/rewards";
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ public void doJndiSetup() {
+ System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.osjava.sj.SimpleContextFactory");
+ System.setProperty("org.osjava.sj.root", "jndi");
+ System.setProperty("org.osjava.jndi.delimiter", "/");
+ System.setProperty("org.osjava.sj.jndi.shared", "true");
+
+ logger.info("Running JDNI setup");
+
+ try {
+ InitialContext ic = new InitialContext();
+
+ // Construct DataSource
+ DataSource ds = new EmbeddedDatabaseBuilder().addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql").build();
+
+ // Bind as a JNDI resource
+ ic.rebind(REWARDS_DB_JNDI_PATH, ds);
+ logger.info("JNDI Resource '" + REWARDS_DB_JNDI_PATH + "' instanceof " + ds.getClass().getSimpleName());
+ } catch (NamingException ex) {
+ logger.error("JNDI setup failed", ex);
+ ex.printStackTrace();
+ System.exit(0);
+ }
+
+ logger.info("JNDI Registrations completed.");
+ }
+
+ /**
+ * Using the BeanFactoryPostProcessor as a convenient entry-point to do setup
+ * before Spring creates any beans.
+ */
+ @Override
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
+ doJndiSetup();
+ return;
+ }
+
+}
diff --git a/lab/24-test/src/test/java/rewards/TestInfrastructureConfig.java b/lab/24-test/src/test/java/rewards/TestInfrastructureConfig.java
new file mode 100644
index 0000000..371cfde
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/TestInfrastructureConfig.java
@@ -0,0 +1,23 @@
+package rewards;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import config.RewardsConfig;
+
+@Configuration
+@Import({
+ TestInfrastructureLocalConfig.class,
+ TestInfrastructureJndiConfig.class,
+ RewardsConfig.class })
+public class TestInfrastructureConfig {
+
+ /**
+ * The bean logging post-processor from the bean lifecycle slides.
+ */
+ @Bean
+ public static LoggingBeanPostProcessor loggingBean(){
+ return new LoggingBeanPostProcessor();
+ }
+}
diff --git a/lab/24-test/src/test/java/rewards/TestInfrastructureJndiConfig.java b/lab/24-test/src/test/java/rewards/TestInfrastructureJndiConfig.java
new file mode 100644
index 0000000..136f6a6
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/TestInfrastructureJndiConfig.java
@@ -0,0 +1,38 @@
+package rewards;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+
+import javax.naming.InitialContext;
+import javax.sql.DataSource;
+
+/**
+ * Sets up a JNDI service for our test.
+ *
+ * See SimpleJndiHelper class to see how this works.
+ */
+@Configuration
+@Profile("jndi")
+public class TestInfrastructureJndiConfig {
+
+ /**
+ * Static method because we are defining a Bean post-processor.
+ */
+ @Bean
+ public static SimpleJndiHelper jndiHelper() {
+ return new SimpleJndiHelper();
+ }
+
+ /**
+ * Create the data-source by doing a JNDI lookup.
+ *
+ * @return The data-source if found
+ * @throws Exception
+ * Any lookup error.
+ */
+ @Bean
+ public DataSource dataSource() throws Exception {
+ return (DataSource) (new InitialContext()).lookup("java:/comp/env/jdbc/rewards");
+ }
+}
diff --git a/lab/24-test/src/test/java/rewards/TestInfrastructureLocalConfig.java b/lab/24-test/src/test/java/rewards/TestInfrastructureLocalConfig.java
new file mode 100644
index 0000000..329b0c9
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/TestInfrastructureLocalConfig.java
@@ -0,0 +1,27 @@
+package rewards;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+/* TODO-05: Update this configuration class so that its
+ * beans are members of the "local" profile.
+ */
+@Configuration
+public class TestInfrastructureLocalConfig {
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/24-test/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/24-test/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..758750c
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,75 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertAll;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import common.money.MonetaryAmount;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ @DisplayName("test if reward computation and distribution works")
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertAll("distribution of reward",
+ () -> assertEquals(2, contribution.getDistributions().size()),
+ () -> assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount()),
+ () -> assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount()));
+
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/test/java/rewards/internal/StubAccountRepository.java b/lab/24-test/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..376289f
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,54 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account
+ * "Keith and Keri Donald" with two beneficiaries "Annabelle" (50% allocation)
+ * and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can
+ * work with this stub and not have to bring in expensive and/or complex
+ * dependencies such as a Database. Simple unit tests can then verify object
+ * behavior by considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ /**
+ * Creates a single test account with two beneficiaries. Also logs creation
+ * so we know which repository we are using.
+ */
+ public StubAccountRepository() {
+ logger.info("Creating " + getClass().getSimpleName());
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/24-test/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..be3df7a
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,50 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant
+ * "Apple Bees" with a 8% benefit availability percentage that's always
+ * available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can
+ * work with this stub and not have to bring in expensive and/or complex
+ * dependencies such as a Database. Simple unit tests can then verify object
+ * behavior by considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ /**
+ * Creates a single test restaurant with an 8% benefit policy. Also logs
+ * creation so we know which repository we are using.
+ */
+ public StubRestaurantRepository() {
+ logger.info("Creating " + getClass().getSimpleName());
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber
+ .get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/test/java/rewards/internal/StubRewardRepository.java b/lab/24-test/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..2546788
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,34 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ /**
+ * Constructor logs creation so we know which repository we are using.
+ */
+ public StubRewardRepository() {
+ logger.info("Creating " + getClass().getSimpleName());
+ }
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/test/java/rewards/internal/account/AccountTests.java b/lab/24-test/src/test/java/rewards/internal/account/AccountTests.java
new file mode 100644
index 0000000..c18c1c9
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/internal/account/AccountTests.java
@@ -0,0 +1,68 @@
+package rewards.internal.account;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.junit.jupiter.api.Test;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import rewards.AccountContribution;
+
+/**
+ * Unit tests for the Account class that verify Account behavior works in isolation.
+ */
+public class AccountTests {
+
+ private final Account account = new Account("1", "Keith and Keri Donald");
+
+ @Test
+ public void accountIsValid() {
+ // setup account with a valid set of beneficiaries to prepare for testing
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ assertTrue(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWithNoBeneficiaries() {
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreOver100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreUnder100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("25%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void makeContribution() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ assertEquals(contribution.getAmount(), MonetaryAmount.valueOf("100.00"));
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+
+ @Test
+ public void throwIllegalStateExceptionWhenContributionIsInvalid() {
+ Throwable exception = assertThrows(IllegalStateException.class,
+ () -> {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ });
+ assertEquals("Cannot make contributions to this account: it has invalid beneficiary allocations", exception.getMessage());
+ }
+}
\ No newline at end of file
diff --git a/lab/24-test/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java b/lab/24-test/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
new file mode 100644
index 0000000..4326571
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
@@ -0,0 +1,96 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC account repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcAccountRepositoryTests {
+
+ private JdbcAccountRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ }
+
+ @Test
+ public void testFindAccountByCreditCard() {
+ Account account = repository.findByCreditCard("1234123412341234");
+ // assert the returned account contains what you expect given the state of the database
+ assertNotNull(account, "account should never be null");
+ assertEquals(Long.valueOf(0), account.getEntityId(), "wrong entity id");
+ assertEquals("123456789", account.getNumber(), "wrong account number");
+ assertEquals("Keith and Keri Donald", account.getName(), "wrong name");
+ assertEquals(2, account.getBeneficiaries().size(), "wrong beneficiary collection size");
+
+ Beneficiary b1 = account.getBeneficiary("Annabelle");
+ assertNotNull(b1, "Annabelle should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b1.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b1.getAllocationPercentage(), "wrong allocation percentage");
+
+ Beneficiary b2 = account.getBeneficiary("Corgan");
+ assertNotNull(b2, "Corgan should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b2.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b2.getAllocationPercentage(), "wrong allocation percentage");
+ }
+
+ @Test
+ public void testFindAccountByCreditCardNoAccount() {
+ assertThrows(EmptyResultDataAccessException.class, () -> {
+ repository.findByCreditCard("bogus");
+ });
+ }
+
+ @Test
+ public void testUpdateBeneficiaries() throws SQLException {
+ Account account = repository.findByCreditCard("1234123412341234");
+ account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ repository.updateBeneficiaries(account);
+ verifyBeneficiaryTableUpdated();
+ }
+
+ private void verifyBeneficiaryTableUpdated() throws SQLException {
+ String sql = "select SAVINGS from T_ACCOUNT_BENEFICIARY where NAME = ? and ACCOUNT_ID = ?";
+ PreparedStatement stmt = dataSource.getConnection().prepareStatement(sql);
+
+ // assert Annabelle has $4.00 savings now
+ stmt.setString(1, "Annabelle");
+ stmt.setLong(2, 0L);
+ ResultSet rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+
+ // assert Corgan has $4.00 savings now
+ stmt.setString(1, "Corgan");
+ stmt.setLong(2, 0L);
+ rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/24-test/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java b/lab/24-test/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
new file mode 100644
index 0000000..3f16cd3
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
@@ -0,0 +1,77 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC restaurant repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRestaurantRepositoryTests {
+
+ private JdbcRestaurantRepository repository;
+
+ @BeforeEach
+ public void setUp() {
+ // simulate the Spring bean initialization lifecycle:
+
+ // first, construct the bean
+ repository = new JdbcRestaurantRepository();
+
+ // then, inject its dependencies
+ repository.setDataSource(createTestDataSource());
+
+ // lastly, initialize the bean
+ repository.populateRestaurantCache();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ // simulate the Spring bean destruction lifecycle:
+
+ // destroy the bean
+ repository.clearRestaurantCache();
+ }
+
+ @Test
+ public void findRestaurantByMerchantNumber() {
+ Restaurant restaurant = repository.findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "the restaurant should never be null");
+ assertEquals("1234567890", restaurant.getNumber(), "the merchant number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "the name is wrong");
+ assertEquals(Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "the benefitPercentage is wrong");
+ }
+
+ @Test
+ public void testFindRestaurantByBogusMerchantNumber() {
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("bogus");
+ });
+ }
+
+ @Test
+ public void restaurantCacheClearedAfterDestroy() {
+ // force early tear down
+ tearDown();
+
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("1234567890");
+ });
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/24-test/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java b/lab/24-test/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
new file mode 100644
index 0000000..b366df4
--- /dev/null
+++ b/lab/24-test/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
@@ -0,0 +1,81 @@
+package rewards.internal.reward;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.Account;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Tests the JDBC reward repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRewardRepositoryTests {
+
+ private JdbcRewardRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRewardRepository();
+ dataSource = createTestDataSource();
+ repository.setDataSource(dataSource);
+ }
+
+ @Test
+ public void testCreateReward() throws SQLException {
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "0123456789");
+
+ Account account = new Account("1", "Keith and Keri Donald");
+ account.setEntityId(0L);
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ RewardConfirmation confirmation = repository.confirmReward(contribution, dining);
+ assertNotNull(confirmation, "confirmation should not be null");
+ assertNotNull(confirmation.getConfirmationNumber(), "confirmation number should not be null");
+ assertEquals(contribution, confirmation.getAccountContribution(), "wrong contribution object");
+ verifyRewardInserted(confirmation, dining);
+ }
+
+ private void verifyRewardInserted(RewardConfirmation confirmation, Dining dining) throws SQLException {
+ assertEquals(1, getRewardCount());
+ Statement stmt = dataSource.getConnection().createStatement();
+ ResultSet rs = stmt.executeQuery("select REWARD_AMOUNT from T_REWARD where CONFIRMATION_NUMBER = '"
+ + confirmation.getConfirmationNumber() + "'");
+ rs.next();
+ assertEquals(confirmation.getAccountContribution().getAmount(), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private int getRewardCount() throws SQLException {
+ Statement stmt = dataSource.getConnection().createStatement();
+ ResultSet rs = stmt.executeQuery("select count(*) from T_REWARD");
+ rs.next();
+ return rs.getInt(1);
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/24-test/src/test/resources/jndi/jndi.properties b/lab/24-test/src/test/resources/jndi/jndi.properties
new file mode 100644
index 0000000..3bfd943
--- /dev/null
+++ b/lab/24-test/src/test/resources/jndi/jndi.properties
@@ -0,0 +1,4 @@
+java.naming.factory.initial=org.osjava.sj.SimpleContextFactory
+org.osjava.sj.root=target/test-classes/config
+org.osjava.jndi.delimiter=/
+org.osjava.sj.jndi.shared=true
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/build.gradle b/lab/26-jdbc-solution/build.gradle
new file mode 100644
index 0000000..af9f0d4
--- /dev/null
+++ b/lab/26-jdbc-solution/build.gradle
@@ -0,0 +1,3 @@
+dependencies {
+ implementation project(':00-rewards-common')
+}
diff --git a/lab/26-jdbc-solution/pom.xml b/lab/26-jdbc-solution/pom.xml
new file mode 100644
index 0000000..d5f53a9
--- /dev/null
+++ b/lab/26-jdbc-solution/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+ 26-jdbc-solution
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
diff --git a/lab/26-jdbc-solution/src/main/java/config/RewardsConfig.java b/lab/26-jdbc-solution/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..ec20e11
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,52 @@
+package config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.jdbc.core.JdbcTemplate;
+import rewards.RewardNetwork;
+import rewards.internal.RewardNetworkImpl;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.account.JdbcAccountRepository;
+import rewards.internal.restaurant.JdbcRestaurantRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.JdbcRewardRepository;
+import rewards.internal.reward.RewardRepository;
+
+import javax.sql.DataSource;
+
+@Configuration
+public class RewardsConfig {
+
+ final JdbcTemplate jdbcTemplate;
+
+ public RewardsConfig(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ @Bean
+ public RewardNetwork rewardNetwork(){
+ return new RewardNetworkImpl(
+ accountRepository(),
+ restaurantRepository(),
+ rewardRepository());
+ }
+
+ @Bean
+ public AccountRepository accountRepository(){
+ JdbcAccountRepository repository = new JdbcAccountRepository(jdbcTemplate);
+ return repository;
+ }
+
+ @Bean
+ public RestaurantRepository restaurantRepository(){
+ JdbcRestaurantRepository repository = new JdbcRestaurantRepository(jdbcTemplate);
+ return repository;
+ }
+
+ @Bean
+ public RewardRepository rewardRepository(){
+ JdbcRewardRepository repository = new JdbcRewardRepository(jdbcTemplate);
+ return repository;
+ }
+
+}
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/AccountContribution.java b/lab/26-jdbc-solution/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/Dining.java b/lab/26-jdbc-solution/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/RewardConfirmation.java b/lab/26-jdbc-solution/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/RewardNetwork.java b/lab/26-jdbc-solution/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..cb3191e
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,52 @@
+package rewards.internal;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/account/Account.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..84463f0
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,159 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ try {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ } catch (IllegalArgumentException e) {
+ // total would have been over 100% - return invalid
+ return false;
+ }
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account. Callers should not attempt to hold on or modify the returned set.
+ * This method should only be used transitively; for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Returns a single account beneficiary. Callers should not attempt to hold on or modify the returned object. This
+ * method should only be used transitively; for example, called to facilitate reporting or testing.
+ * @param name the name of the beneficiary e.g "Annabelle"
+ * @return the beneficiary object
+ */
+ public Beneficiary getBeneficiary(String name) {
+ for (Beneficiary b : beneficiaries) {
+ if (b.getName().equals(name)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("No such beneficiary with name '" + name + "'");
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/account/AccountRepository.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/account/Beneficiary.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..06e2a4e
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,112 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.ResultSetExtractor;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+public class JdbcAccountRepository implements AccountRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public JdbcAccountRepository(JdbcTemplate jdbcTemplate) {
+
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ /**
+ * Extracts an Account object from rows returned from a join of T_ACCOUNT and
+ * T_ACCOUNT_BENEFICIARY.
+ */
+ private final ResultSetExtractor accountExtractor = new AccountExtractor();
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = """
+ select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME,\
+ c.NUMBER as CREDIT_CARD_NUMBER,\
+ b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE,\
+ b.SAVINGS as BENEFICIARY_SAVINGS\
+ from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c\
+ where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?\
+ """;
+ return jdbcTemplate.query(sql, accountExtractor, creditCardNumber);
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ for (Beneficiary b : account.getBeneficiaries()) {
+ jdbcTemplate.update(sql, b.getSavings().asBigDecimal(), account.getEntityId(), b.getName());
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to
+ * a fully-reconstituted Account aggregate.
+ *
+ * @param rs
+ * the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException
+ * an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+
+ Beneficiary beneficiary = mapBeneficiary(rs);
+ if (beneficiary != null)
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary
+ * object.
+ *
+ * @param rs
+ * the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException
+ * an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+
+ if (name == null)
+ return null; // No beneficiary
+
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+
+ private class AccountExtractor implements ResultSetExtractor {
+
+ public Account extractData(ResultSet rs) throws SQLException, DataAccessException {
+ return mapAccount(rs);
+ }
+ }
+}
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/account/package.html b/lab/26-jdbc-solution/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
new file mode 100644
index 0000000..b7d6d74
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
@@ -0,0 +1,20 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * Determines if benefit is available for an account for dining.
+ *
+ * A value object. A strategy. Scoped by the Resturant aggregate.
+ */
+public interface BenefitAvailabilityPolicy {
+
+ /**
+ * Calculates if an account is eligible to receive benefits for a dining.
+ * @param account the account of the member who dined
+ * @param dining the dining event
+ * @return benefit availability status
+ */
+ boolean isBenefitAvailableFor(Account account, Dining dining);
+}
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..aa4a789
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,114 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ */
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public JdbcRestaurantRepository(JdbcTemplate jdbcTemplate) {
+
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ */
+ private final RowMapper rowMapper = new RestaurantRowMapper();
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ String sql = "select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY from T_RESTAURANT where MERCHANT_NUMBER = ?";
+ return jdbcTemplate.queryForObject(sql, rowMapper, merchantNumber);
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+ // map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ restaurant.setBenefitAvailabilityPolicy(mapBenefitAvailabilityPolicy(rs));
+ return restaurant;
+ }
+
+ /**
+ * Helper method that maps benefit availability policy data in the ResultSet to a fully-configured
+ * {@link BenefitAvailabilityPolicy} object. The key column is 'BENEFIT_AVAILABILITY_POLICY', which is a
+ * discriminator column containing a string code that identifies the type of policy. Currently supported types are:
+ * 'A' for 'always available' and 'N' for 'never available'.
+ *
+ *
+ * More types could be added easily by enhancing this method. For example, 'W' for 'Weekdays only' or 'M' for 'Max
+ * Rewards per Month'. Some of these types might require additional database column values to be configured, for
+ * example a 'MAX_REWARDS_PER_MONTH' data column.
+ *
+ * @param rs the result set used to map the policy object from database column values
+ * @return the matching benefit availability policy
+ * @throws IllegalArgumentException if the mapping could not be performed
+ */
+ private BenefitAvailabilityPolicy mapBenefitAvailabilityPolicy(ResultSet rs) throws SQLException {
+ String policyCode = rs.getString("BENEFIT_AVAILABILITY_POLICY");
+ if ("A".equals(policyCode)) {
+ return AlwaysAvailable.INSTANCE;
+ } else if ("N".equals(policyCode)) {
+ return NeverAvailable.INSTANCE;
+ } else {
+ throw new IllegalArgumentException("Not a supported policy code " + policyCode);
+ }
+ }
+
+ /**
+ * Returns true indicating benefit is always available.
+ */
+ static class AlwaysAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new AlwaysAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+
+ public String toString() {
+ return "alwaysAvailable";
+ }
+ }
+
+ /**
+ * Returns false indicating benefit is never available.
+ */
+ static class NeverAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new NeverAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return false;
+ }
+
+ public String toString() {
+ return "neverAvailable";
+ }
+ }
+
+ private class RestaurantRowMapper implements RowMapper {
+
+ public Restaurant mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return mapRestaurant(rs);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..2e73e57
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,102 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on an availability policy and a
+ * benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ private BenefitAvailabilityPolicy benefitAvailabilityPolicy;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Sets the policy that determines if a dining by an account at this restaurant is eligible for benefit.
+ * @param benefitAvailabilityPolicy the benefit availability policy
+ */
+ public void setBenefitAvailabilityPolicy(BenefitAvailabilityPolicy benefitAvailabilityPolicy) {
+ this.benefitAvailabilityPolicy = benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Returns this restaurant's benefit availability policy.
+ */
+ public BenefitAvailabilityPolicy getBenefitAvailabilityPolicy() {
+ return benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ if (benefitAvailabilityPolicy.isBenefitAvailableFor(account, dining)) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ } else {
+ return MonetaryAmount.zero();
+ }
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage
+ + ", benefitAvailabilityPolicy = " + benefitAvailabilityPolicy;
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/package.html b/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..01d4fd3
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,35 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import org.springframework.jdbc.core.JdbcTemplate;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * JDBC implementation of a reward repository that records the result of a reward transaction by inserting a reward
+ * confirmation record.
+ */
+public class JdbcRewardRepository implements RewardRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public JdbcRewardRepository(JdbcTemplate jdbcTemplate) {
+
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ String confirmationNumber = nextConfirmationNumber();
+ jdbcTemplate.update(sql, confirmationNumber, contribution.getAmount().asBigDecimal(),
+ SimpleDate.today().asDate(), contribution.getAccountNumber(), dining.getMerchantNumber(),
+ dining.getDate().asDate(), dining.getAmount().asBigDecimal());
+ return new RewardConfirmation(confirmationNumber, contribution);
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ return jdbcTemplate.queryForObject(sql, String.class);
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/26-jdbc-solution/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..1207f0f
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/main/java/rewards/internal/reward/package.html b/lab/26-jdbc-solution/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/26-jdbc-solution/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/RewardNetworkTests.java b/lab/26-jdbc-solution/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..8597f35
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,53 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application work together to reward for dining
+ * successfully. Uses Spring to bootstrap the application for use in a test environment.
+ */
+@SpringJUnitConfig(classes = {SystemTestConfig.class})
+public class RewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/SystemTestConfig.java b/lab/26-jdbc-solution/src/test/java/rewards/SystemTestConfig.java
new file mode 100644
index 0000000..fbc4dec
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/SystemTestConfig.java
@@ -0,0 +1,31 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import config.RewardsConfig;
+
+
+@Configuration
+@Import(RewardsConfig.class)
+public class SystemTestConfig {
+
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql")
+ .build();
+ }
+
+}
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/26-jdbc-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..98b7353
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,72 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/internal/StubAccountRepository.java b/lab/26-jdbc-solution/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..b926be2
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,43 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/26-jdbc-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..418516d
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,53 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+import rewards.internal.restaurant.BenefitAvailabilityPolicy;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new AlwaysReturnsTrue());
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy that always returns true. Only useful for testing--a real
+ * availability policy might consider many factors such as the day of week of the dining, or the account's reward
+ * history for the current month.
+ */
+ private static class AlwaysReturnsTrue implements BenefitAvailabilityPolicy {
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/internal/StubRewardRepository.java b/lab/26-jdbc-solution/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..2487aca
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/internal/account/AccountTests.java b/lab/26-jdbc-solution/src/test/java/rewards/internal/account/AccountTests.java
new file mode 100644
index 0000000..4075654
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/internal/account/AccountTests.java
@@ -0,0 +1,57 @@
+package rewards.internal.account;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for the Account class that verify Account behavior works in isolation.
+ */
+public class AccountTests {
+
+ private final Account account = new Account("1", "Keith and Keri Donald");
+
+ @Test
+ public void accountIsValid() {
+ // setup account with a valid set of beneficiaries to prepare for testing
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ assertTrue(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWithNoBeneficiaries() {
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreOver100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreUnder100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("25%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void makeContribution() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ assertEquals(contribution.getAmount(), MonetaryAmount.valueOf("100.00"));
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java b/lab/26-jdbc-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
new file mode 100644
index 0000000..15e0fa1
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
@@ -0,0 +1,102 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC account repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcAccountRepositoryTests {
+
+ private JdbcAccountRepository repository;
+
+ private DataSource dataSource;
+
+ private JdbcTemplate jdbcTemplate;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ repository = new JdbcAccountRepository(createTestJdbcTemplate());
+ }
+
+ @Test
+ public void testFindAccountByCreditCard() {
+ Account account = repository.findByCreditCard("1234123412341234");
+ // assert the returned account contains what you expect given the state of the database
+ assertNotNull(account, "account should never be null");
+ assertEquals(Long.valueOf(0), account.getEntityId(), "wrong entity id");
+ assertEquals("123456789", account.getNumber(), "wrong account number");
+ assertEquals("Keith and Keri Donald", account.getName(), "wrong name");
+ assertEquals(2, account.getBeneficiaries().size(), "wrong beneficiary collection size");
+
+ Beneficiary b1 = account.getBeneficiary("Annabelle");
+ assertNotNull(b1, "Annabelle should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b1.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b1.getAllocationPercentage(), "wrong allocation percentage");
+
+ Beneficiary b2 = account.getBeneficiary("Corgan");
+ assertNotNull(b2, "Corgan should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b2.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b2.getAllocationPercentage(), "wrong allocation percentage");
+ }
+
+ @Test
+ public void testFindAccountByCreditCardNoAccount() {
+ assertThrows(EmptyResultDataAccessException.class, () -> {
+ repository.findByCreditCard("bogus");
+ });
+ }
+
+ @Test
+ public void testUpdateBeneficiaries() throws SQLException {
+ Account account = repository.findByCreditCard("1234123412341234");
+ account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ repository.updateBeneficiaries(account);
+ verifyBeneficiaryTableUpdated();
+ }
+
+ private void verifyBeneficiaryTableUpdated() throws SQLException {
+ String sql = "select SAVINGS from T_ACCOUNT_BENEFICIARY where NAME = ? and ACCOUNT_ID = ?";
+ PreparedStatement stmt = dataSource.getConnection().prepareStatement(sql);
+
+ // assert Annabelle has $4.00 savings now
+ stmt.setString(1, "Annabelle");
+ stmt.setLong(2, 0L);
+ ResultSet rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+
+ // assert Corgan has $4.00 savings now
+ stmt.setString(1, "Corgan");
+ stmt.setLong(2, 0L);
+ rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+
+ private JdbcTemplate createTestJdbcTemplate() {
+ return new JdbcTemplate(createTestDataSource());
+ }
+}
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java b/lab/26-jdbc-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
new file mode 100644
index 0000000..29c0996
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
@@ -0,0 +1,56 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC restaurant repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRestaurantRepositoryTests {
+
+ private JdbcRestaurantRepository repository;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRestaurantRepository(createTestJdbcTemplate());
+ }
+
+ @Test
+ public void testFindRestaurantByMerchantNumber() {
+ Restaurant restaurant = repository.findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "the restaurant should never be null");
+ assertEquals("1234567890", restaurant.getNumber(), "the merchant number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "the name is wrong");
+ assertEquals(Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "the benefitPercentage is wrong");
+ assertEquals(JdbcRestaurantRepository.AlwaysAvailable.INSTANCE,
+ restaurant.getBenefitAvailabilityPolicy(), "the benefit availability policy is wrong");
+ }
+
+ @Test
+ public void testFindRestaurantByBogusMerchantNumber() {
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("bogus");
+ });
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+
+ private JdbcTemplate createTestJdbcTemplate() {
+ return new JdbcTemplate(createTestDataSource());
+ }
+}
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java b/lab/26-jdbc-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java
new file mode 100644
index 0000000..93c2496
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java
@@ -0,0 +1,71 @@
+package rewards.internal.restaurant;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for exercising the behavior of the Restaurant aggregate entity. A restaurant calculates a benefit to award
+ * to an account for dining based on an availability policy and benefit percentage.
+ */
+public class RestaurantTests {
+
+ private Restaurant restaurant;
+
+ private Account account;
+
+ private Dining dining;
+
+ @BeforeEach
+ public void setUp() {
+ // configure the restaurant, the object being tested
+ restaurant = new Restaurant("1234567890", "AppleBee's");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(true));
+ // configure supporting objects needed by the restaurant
+ account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle");
+ dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+ }
+
+ @Test
+ public void testCalcuateBenefitFor() {
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert 8.00 eligible for reward
+ assertEquals(MonetaryAmount.valueOf("8.00"), benefit);
+ }
+
+ @Test
+ public void testNoBenefitAvailable() {
+ // configure stub that always returns false
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(false));
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert zero eligible for reward
+ assertEquals(MonetaryAmount.valueOf("0.00"), benefit);
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy containing a single flag used to determine if benefit is available.
+ * Only useful for testing--a real availability policy might consider many factors such as the day of week of the
+ * dining, or the account's reward history for the current month.
+ */
+ private static class StubBenefitAvailibilityPolicy implements BenefitAvailabilityPolicy {
+
+ private final boolean isBenefitAvailable;
+
+ public StubBenefitAvailibilityPolicy(boolean isBenefitAvailable) {
+ this.isBenefitAvailable = isBenefitAvailable;
+ }
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return isBenefitAvailable;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java b/lab/26-jdbc-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
new file mode 100644
index 0000000..dcd58f7
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
@@ -0,0 +1,93 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.Account;
+
+import javax.sql.DataSource;
+import java.math.BigDecimal;
+import java.sql.SQLException;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Tests the JDBC reward repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRewardRepositoryTests {
+
+ private JdbcRewardRepository repository;
+
+ private DataSource dataSource;
+
+ private JdbcTemplate jdbcTemplate;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ jdbcTemplate = createTestJdbcTemplate();
+ repository = new JdbcRewardRepository(jdbcTemplate);
+
+ }
+
+ @Test
+ public void testCreateReward() throws SQLException {
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "0123456789");
+
+ Account account = new Account("1", "Keith and Keri Donald");
+ account.setEntityId(0L);
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ RewardConfirmation confirmation = repository.confirmReward(contribution, dining);
+ assertNotNull(confirmation, "confirmation should not be null");
+ assertNotNull(confirmation.getConfirmationNumber(), "confirmation number should not be null");
+ assertEquals(contribution, confirmation.getAccountContribution(), "wrong contribution object");
+ verifyRewardInserted(confirmation, dining);
+ }
+
+ private void verifyRewardInserted(RewardConfirmation confirmation, Dining dining) {
+ assertEquals(1, getRewardCount());
+ String sql = "select * from T_REWARD where CONFIRMATION_NUMBER = ?";
+ Map values = jdbcTemplate.queryForMap(sql, confirmation.getConfirmationNumber());
+ verifyInsertedValues(confirmation, dining, values);
+ }
+
+ private void verifyInsertedValues(RewardConfirmation confirmation, Dining dining, Map values) {
+ assertEquals(confirmation.getAccountContribution().getAmount(), new MonetaryAmount((BigDecimal) values
+ .get("REWARD_AMOUNT")));
+ assertEquals(SimpleDate.today().asDate(), values.get("REWARD_DATE"));
+ assertEquals(confirmation.getAccountContribution().getAccountNumber(), values.get("ACCOUNT_NUMBER"));
+ assertEquals(dining.getAmount(), new MonetaryAmount((BigDecimal) values.get("DINING_AMOUNT")));
+ assertEquals(dining.getMerchantNumber(), values.get("DINING_MERCHANT_NUMBER"));
+ assertEquals(SimpleDate.today().asDate(), values.get("DINING_DATE"));
+ }
+
+ private int getRewardCount() {
+ String sql = "select count(*) from T_REWARD";
+ return jdbcTemplate.queryForObject(sql, Integer.class);
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+
+ private JdbcTemplate createTestJdbcTemplate() {
+ return new JdbcTemplate(createTestDataSource());
+ }
+}
diff --git a/lab/26-jdbc-solution/src/test/resources/rewards/testdb/data.sql b/lab/26-jdbc-solution/src/test/resources/rewards/testdb/data.sql
new file mode 100644
index 0000000..28a87cc
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/resources/rewards/testdb/data.sql
@@ -0,0 +1,78 @@
+
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456789', 'Keith and Keri Donald');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456001', 'Dollie R. Adams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456002', 'Cornelia J. Andresen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456003', 'Coral Villareal Betancourt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456004', 'Chad I. Cobbs');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456005', 'Michael C. Feller');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456006', 'Michael J. Grover');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456007', 'John C. Howard');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456008', 'Ida Ketterer');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456009', 'Laina Ochoa Lucero');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456010', 'Wesley M. Mayo');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456011', 'Leslie F. Mcclary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456012', 'John D. Mudra');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456013', 'Pietronella J. Nielsen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456014', 'John S. Oleary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456015', 'Glenda D. Smith');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456016', 'Willemina O. Thygesen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456017', 'Antje Vogt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456018', 'Julia Weber');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456019', 'Mark T. Williams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456020', 'Christine J. Wilson');
+
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (0, '1234123412341234');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (1, '1234123412340001');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (2, '1234123412340002');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (3, '1234123412340003');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (4, '1234123412340004');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (5, '1234123412340005');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (6, '1234123412340006');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (7, '1234123412340007');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (8, '1234123412340008');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (9, '1234123412340009');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (10, '1234123412340010');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (11, '1234123412340011');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (12, '1234123412340012');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (13, '1234123412340013');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (14, '1234123412340014');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (15, '1234123412340015');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (16, '1234123412340016');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (17, '1234123412340017');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (18, '1234123412340018');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (19, '1234123412340019');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (20, '1234123412340020');
+
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (0, 'Annabelle', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (0, 'Corgan', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Antolin', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Argus', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Gian', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Argeo', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Kai', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Kasper', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Ernst', .34, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (12, 'Brian', .75, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (12, 'Shelby', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Charles', .50, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Thomas', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Neil', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (17, 'Daniel', 1.0, 0.00);
+
+insert into T_RESTAURANT (MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY)
+ values ('1234567890', 'AppleBees', .08, 'A');
diff --git a/lab/26-jdbc-solution/src/test/resources/rewards/testdb/schema.sql b/lab/26-jdbc-solution/src/test/resources/rewards/testdb/schema.sql
new file mode 100644
index 0000000..b0324fa
--- /dev/null
+++ b/lab/26-jdbc-solution/src/test/resources/rewards/testdb/schema.sql
@@ -0,0 +1,20 @@
+drop table T_ACCOUNT_BENEFICIARY if exists;
+drop table T_ACCOUNT_CREDIT_CARD if exists;
+drop table T_ACCOUNT if exists;
+drop table T_RESTAURANT if exists;
+drop table T_REWARD if exists;
+drop sequence S_REWARD_CONFIRMATION_NUMBER if exists;
+drop table DUAL_REWARD_CONFIRMATION_NUMBER if exists;
+
+create table T_ACCOUNT (ID integer identity primary key, NUMBER varchar(9), NAME varchar(50) not null, unique(NUMBER));
+create table T_ACCOUNT_CREDIT_CARD (ID integer identity primary key, ACCOUNT_ID integer, NUMBER varchar(16), unique(ACCOUNT_ID, NUMBER));
+create table T_ACCOUNT_BENEFICIARY (ID integer identity primary key, ACCOUNT_ID integer, NAME varchar(50), ALLOCATION_PERCENTAGE decimal(3,2) not null, SAVINGS decimal(8,2) not null, unique(ACCOUNT_ID, NAME));
+create table T_RESTAURANT (ID integer identity primary key, MERCHANT_NUMBER varchar(10) not null, NAME varchar(80) not null, BENEFIT_PERCENTAGE decimal(3,2) not null, BENEFIT_AVAILABILITY_POLICY varchar(1) not null, unique(MERCHANT_NUMBER));
+create table T_REWARD (ID integer identity primary key, CONFIRMATION_NUMBER varchar(25) not null, REWARD_AMOUNT decimal(8,2) not null, REWARD_DATE date not null, ACCOUNT_NUMBER varchar(9) not null, DINING_AMOUNT decimal not null, DINING_MERCHANT_NUMBER varchar(10) not null, DINING_DATE date not null, unique(CONFIRMATION_NUMBER));
+
+create sequence S_REWARD_CONFIRMATION_NUMBER start with 1;
+create table DUAL_REWARD_CONFIRMATION_NUMBER (ZERO integer);
+insert into DUAL_REWARD_CONFIRMATION_NUMBER values (0);
+
+alter table T_ACCOUNT_CREDIT_CARD add constraint FK_ACCOUNT_CREDIT_CARD foreign key (ACCOUNT_ID) references T_ACCOUNT(ID) on delete cascade;
+alter table T_ACCOUNT_BENEFICIARY add constraint FK_ACCOUNT_BENEFICIARY foreign key (ACCOUNT_ID) references T_ACCOUNT(ID) on delete cascade;
\ No newline at end of file
diff --git a/lab/26-jdbc/build.gradle b/lab/26-jdbc/build.gradle
new file mode 100644
index 0000000..76eed19
--- /dev/null
+++ b/lab/26-jdbc/build.gradle
@@ -0,0 +1,7 @@
+dependencies {
+ implementation project(':00-rewards-common')
+}
+
+test {
+ exclude '**/JdbcRewardRepositoryTests.class'
+}
diff --git a/lab/26-jdbc/pom.xml b/lab/26-jdbc/pom.xml
new file mode 100644
index 0000000..afab982
--- /dev/null
+++ b/lab/26-jdbc/pom.xml
@@ -0,0 +1,35 @@
+
+
+ 4.0.0
+ 26-jdbc
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ **/JdbcRewardRepositoryTests.java
+
+
+
+
+
+
diff --git a/lab/26-jdbc/src/main/java/config/RewardsConfig.java b/lab/26-jdbc/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..56431c5
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,50 @@
+package config;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import rewards.RewardNetwork;
+import rewards.internal.RewardNetworkImpl;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.account.JdbcAccountRepository;
+import rewards.internal.restaurant.JdbcRestaurantRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.JdbcRewardRepository;
+import rewards.internal.reward.RewardRepository;
+
+@Configuration
+public class RewardsConfig {
+
+ @Autowired
+ DataSource dataSource;
+
+ @Bean
+ public RewardNetwork rewardNetwork(){
+ return new RewardNetworkImpl(
+ accountRepository(),
+ restaurantRepository(),
+ rewardRepository());
+ }
+
+ @Bean
+ public AccountRepository accountRepository(){
+ JdbcAccountRepository repository = new JdbcAccountRepository(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RestaurantRepository restaurantRepository(){
+ JdbcRestaurantRepository repository = new JdbcRestaurantRepository(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RewardRepository rewardRepository(){
+ JdbcRewardRepository repository = new JdbcRewardRepository(dataSource);
+ return repository;
+ }
+
+}
diff --git a/lab/26-jdbc/src/main/java/rewards/AccountContribution.java b/lab/26-jdbc/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/Dining.java b/lab/26-jdbc/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/RewardConfirmation.java b/lab/26-jdbc/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/RewardNetwork.java b/lab/26-jdbc/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/26-jdbc/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..cb3191e
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,52 @@
+package rewards.internal;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/account/Account.java b/lab/26-jdbc/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..84463f0
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,159 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ try {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ } catch (IllegalArgumentException e) {
+ // total would have been over 100% - return invalid
+ return false;
+ }
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account. Callers should not attempt to hold on or modify the returned set.
+ * This method should only be used transitively; for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Returns a single account beneficiary. Callers should not attempt to hold on or modify the returned object. This
+ * method should only be used transitively; for example, called to facilitate reporting or testing.
+ * @param name the name of the beneficiary e.g "Annabelle"
+ * @return the beneficiary object
+ */
+ public Beneficiary getBeneficiary(String name) {
+ for (Beneficiary b : beneficiaries) {
+ if (b.getName().equals(name)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("No such beneficiary with name '" + name + "'");
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/account/AccountRepository.java b/lab/26-jdbc/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/account/Beneficiary.java b/lab/26-jdbc/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/26-jdbc/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..ca3a566
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,150 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+
+// TODO-10 (Optional) : Inject JdbcTemplate directly to this repository class
+// - Refactor the constructor to get the JdbcTemplate injected directly
+// (instead of DataSource getting injected)
+// - Refactor RewardsConfig accordingly
+// - Refactor JdbcAccountRepositoryTests accordingly
+// - Run JdbcAccountRepositoryTests and verity it passes
+
+// TODO-05: Refactor this repository to use JdbcTemplate.
+// - Add a field of type JdbcTemplate.
+// - Refactor the code in the constructor to instantiate the JdbcTemplate
+// object using the given DataSource object.
+public class JdbcAccountRepository implements AccountRepository {
+
+ private final DataSource dataSource;
+
+ public JdbcAccountRepository(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ // TODO-07 (Optional): Refactor this method using JdbcTemplate and ResultSetExtractor
+ // - Create a ResultSetExtractor object and pass it as an argument
+ // to jdbcTemplate.query(..) method
+ // - Let the extractData() method of the ResultSetExtractor to call
+ // mapAccount() method, which is provided in this class, to do all the work.
+ // - Run the JdbcAccountRepositoryTests class. It should pass.
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = """
+ select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, \
+ b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS \
+ from T_ACCOUNT a, T_ACCOUNT_CREDIT_CARD c \
+ left outer join T_ACCOUNT_BENEFICIARY b \
+ on a.ID = b.ACCOUNT_ID \
+ where c.ACCOUNT_ID = a.ID and c.NUMBER = ?\
+ """;
+
+ Account account = null;
+ Connection conn = null;
+ PreparedStatement ps = null;
+ ResultSet rs = null;
+ try {
+ conn = dataSource.getConnection();
+ ps = conn.prepareStatement(sql);
+ ps.setString(1, creditCardNumber);
+ rs = ps.executeQuery();
+ account = mapAccount(rs);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred finding by credit card number", e);
+ } finally {
+ if (rs != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ rs.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (ps != null) {
+ try {
+ // Close to prevent database cursor exhaustion
+ ps.close();
+ } catch (SQLException ex) {
+ }
+ }
+ if (conn != null) {
+ try {
+ // Close to prevent database connection exhaustion
+ conn.close();
+ } catch (SQLException ex) {
+ }
+ }
+ }
+ return account;
+ }
+
+ // TODO-06: Refactor this method to use JdbcTemplate.
+ // - Note that an account has multiple beneficiaries
+ // and you are going to perform UPDATE operation using
+ // JdbcTemplate for each of those beneficiaries
+ // - Rerun the JdbcAccountRepositoryTests and verify it passes
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+ for (Beneficiary beneficiary : account.getBeneficiaries()) {
+ ps.setBigDecimal(1, beneficiary.getSavings().asBigDecimal());
+ ps.setLong(2, account.getEntityId());
+ ps.setString(3, beneficiary.getName());
+ ps.executeUpdate();
+ }
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred updating beneficiary savings", e);
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ *
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+}
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/account/package.html b/lab/26-jdbc/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java b/lab/26-jdbc/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
new file mode 100644
index 0000000..b7d6d74
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
@@ -0,0 +1,20 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * Determines if benefit is available for an account for dining.
+ *
+ * A value object. A strategy. Scoped by the Resturant aggregate.
+ */
+public interface BenefitAvailabilityPolicy {
+
+ /**
+ * Calculates if an account is eligible to receive benefits for a dining.
+ * @param account the account of the member who dined
+ * @param dining the dining event
+ * @return benefit availability status
+ */
+ boolean isBenefitAvailableFor(Account account, Dining dining);
+}
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/26-jdbc/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..c8b155e
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,150 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.springframework.dao.EmptyResultDataAccessException;
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ */
+
+// TODO-09 (Optional) : Inject JdbcTemplate directly to this repository class
+// - Refactor the constructor to get the JdbcTemplate injected directly
+// (instead of DataSource getting injected)
+// - Refactor RewardsConfig accordingly
+// - Refactor JdbcRestaurantRepositoryTests accordingly
+// - Run JdbcRestaurantRepositoryTests and verity it passes
+
+// TODO-04: Refactor the cumbersome low-level JDBC code to use JdbcTemplate.
+// - Run JdbcRestaurantRepositoryTests and verity it passes
+// - Add a field of type JdbcTemplate
+// - Refactor the code in the constructor to instantiate JdbcTemplate object
+// from the given DataSource object
+// - Refactor findByMerchantNumber(..) to use the JdbcTemplate and a RowMapper
+//
+// #1: Create a RowMapper object and pass it to the
+// jdbcTemplate.queryForObject(..) method as an argument
+// #2: The mapRestaurant(..) method provided in this class contains
+// logic, which the RowMapper may wish to use
+//
+// - Run JdbcRestaurantRepositoryTests again and verity it passes
+
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private final DataSource dataSource;
+
+ public JdbcRestaurantRepository(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ String sql = """
+ select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY\
+ from T_RESTAURANT where MERCHANT_NUMBER = ?\
+ """;
+ Restaurant restaurant = null;
+
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql) ){
+ ps.setString(1, merchantNumber);
+ ResultSet rs = ps.executeQuery();
+ advanceToNextRow(rs);
+ restaurant = mapRestaurant(rs);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred finding by merchant number", e);
+ }
+
+ return restaurant;
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ * @param rs the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // Get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+
+ // Map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ restaurant.setBenefitAvailabilityPolicy(mapBenefitAvailabilityPolicy(rs));
+ return restaurant;
+ }
+
+ /**
+ * Advances a ResultSet to the next row and throws an exception if there are no rows.
+ * @param rs the ResultSet to advance
+ * @throws EmptyResultDataAccessException if there is no next row
+ * @throws SQLException
+ */
+ private void advanceToNextRow(ResultSet rs) throws EmptyResultDataAccessException, SQLException {
+ if (!rs.next()) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ }
+
+ /**
+ * Helper method that maps benefit availability policy data in the ResultSet to a fully-configured
+ * {@link BenefitAvailabilityPolicy} object. The key column is 'BENEFIT_AVAILABILITY_POLICY', which is a
+ * discriminator column containing a string code that identifies the type of policy. Currently supported types are:
+ * 'A' for 'always available' and 'N' for 'never available'.
+ *
+ * More types could be added easily by enhancing this method. For example, 'W' for 'Weekdays only' or 'M' for 'Max
+ * Rewards per Month'. Some of these types might require additional database column values to be configured, for
+ * example a 'MAX_REWARDS_PER_MONTH' data column.
+ *
+ * @param rs the result set used to map the policy object from database column values
+ * @return the matching benefit availability policy
+ * @throws IllegalArgumentException if the mapping could not be performed
+ */
+ private BenefitAvailabilityPolicy mapBenefitAvailabilityPolicy(ResultSet rs) throws SQLException {
+ String policyCode = rs.getString("BENEFIT_AVAILABILITY_POLICY");
+ if ("A".equals(policyCode)) {
+ return AlwaysAvailable.INSTANCE;
+ } else if ("N".equals(policyCode)) {
+ return NeverAvailable.INSTANCE;
+ } else {
+ throw new IllegalArgumentException("Not a supported policy code " + policyCode);
+ }
+ }
+
+ /**
+ * Returns true indicating benefit is always available.
+ */
+ static class AlwaysAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new AlwaysAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+
+ public String toString() {
+ return "alwaysAvailable";
+ }
+ }
+
+ /**
+ * Returns false indicating benefit is never available.
+ */
+ static class NeverAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new NeverAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return false;
+ }
+
+ public String toString() {
+ return "neverAvailable";
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/26-jdbc/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..2e73e57
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,102 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on an availability policy and a
+ * benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ private BenefitAvailabilityPolicy benefitAvailabilityPolicy;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Sets the policy that determines if a dining by an account at this restaurant is eligible for benefit.
+ * @param benefitAvailabilityPolicy the benefit availability policy
+ */
+ public void setBenefitAvailabilityPolicy(BenefitAvailabilityPolicy benefitAvailabilityPolicy) {
+ this.benefitAvailabilityPolicy = benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Returns this restaurant's benefit availability policy.
+ */
+ public BenefitAvailabilityPolicy getBenefitAvailabilityPolicy() {
+ return benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ if (benefitAvailabilityPolicy.isBenefitAvailableFor(account, dining)) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ } else {
+ return MonetaryAmount.zero();
+ }
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage
+ + ", benefitAvailabilityPolicy = " + benefitAvailabilityPolicy;
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/26-jdbc/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/restaurant/package.html b/lab/26-jdbc/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/26-jdbc/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..6b8d7e7
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,83 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+import javax.sql.DataSource;
+import java.sql.*;
+
+/**
+ * JDBC implementation of a reward repository that records the result
+ * of a reward transaction by inserting a reward confirmation record.
+ */
+
+// TODO-08 (Optional) : Inject JdbcTemplate directly to this repository class
+// - Refactor the constructor to get the JdbcTemplate injected directly
+// (instead of DataSource getting injected)
+// - Refactor RewardsConfig accordingly
+// - Refactor JdbcRewardRepositoryTests accordingly
+// - Run JdbcRewardRepositoryTests and verity it passes
+
+// TODO-03: Refactor the cumbersome low-level JDBC code in JdbcRewardRepository with JdbcTemplate.
+// - Add a field of type JdbcTemplate.
+// - Refactor the code in the constructor to instantiate JdbcTemplate
+// object from the given DataSource object.
+// - Refactor the confirmReward(...) and nextConfirmationNumber() methods to use
+// the JdbcTemplate object.
+//
+// DO NOT delete the old JDBC code, just comment out the try/catch block.
+// You will need to refer to the old JDBC code to write the new code.
+//
+// - Run JdbcRewardRepositoryTests and verity it passes
+// (If you are using Gradle, make sure to comment out the exclude statement
+// in the test task in the build.gradle.)
+
+public class JdbcRewardRepository implements RewardRepository {
+
+ private final DataSource dataSource;
+
+ public JdbcRewardRepository(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ String confirmationNumber = nextConfirmationNumber();
+
+ // Update the T_REWARD table with the new Reward
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql)) {
+
+ ps.setString(1, confirmationNumber);
+ ps.setBigDecimal(2, contribution.getAmount().asBigDecimal());
+ ps.setDate(3, new Date(SimpleDate.today().inMilliseconds()));
+ ps.setString(4, contribution.getAccountNumber());
+ ps.setString(5, dining.getMerchantNumber());
+ ps.setDate(6, new Date(dining.getDate().inMilliseconds()));
+ ps.setBigDecimal(7, dining.getAmount().asBigDecimal());
+ ps.execute();
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception occurred inserting reward record", e);
+ }
+
+ return new RewardConfirmation(confirmationNumber, contribution);
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ String nextValue;
+
+ try (Connection conn = dataSource.getConnection();
+ PreparedStatement ps = conn.prepareStatement(sql);
+ ResultSet rs = ps.executeQuery()) {
+ rs.next();
+ nextValue = rs.getString(1);
+ } catch (SQLException e) {
+ throw new RuntimeException("SQL exception getting next confirmation number", e);
+ }
+
+ return nextValue;
+ }
+}
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/26-jdbc/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..1207f0f
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/main/java/rewards/internal/reward/package.html b/lab/26-jdbc/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/26-jdbc/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/26-jdbc/src/test/java/rewards/RewardNetworkTests.java b/lab/26-jdbc/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..8597f35
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,53 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application work together to reward for dining
+ * successfully. Uses Spring to bootstrap the application for use in a test environment.
+ */
+@SpringJUnitConfig(classes = {SystemTestConfig.class})
+public class RewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/test/java/rewards/SystemTestConfig.java b/lab/26-jdbc/src/test/java/rewards/SystemTestConfig.java
new file mode 100644
index 0000000..fbc4dec
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/SystemTestConfig.java
@@ -0,0 +1,31 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import config.RewardsConfig;
+
+
+@Configuration
+@Import(RewardsConfig.class)
+public class SystemTestConfig {
+
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql")
+ .build();
+ }
+
+}
diff --git a/lab/26-jdbc/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/26-jdbc/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..98b7353
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,72 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/test/java/rewards/internal/StubAccountRepository.java b/lab/26-jdbc/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..b926be2
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,43 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/26-jdbc/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..418516d
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,53 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+import rewards.internal.restaurant.BenefitAvailabilityPolicy;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new AlwaysReturnsTrue());
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy that always returns true. Only useful for testing--a real
+ * availability policy might consider many factors such as the day of week of the dining, or the account's reward
+ * history for the current month.
+ */
+ private static class AlwaysReturnsTrue implements BenefitAvailabilityPolicy {
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/test/java/rewards/internal/StubRewardRepository.java b/lab/26-jdbc/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..2487aca
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/test/java/rewards/internal/account/AccountTests.java b/lab/26-jdbc/src/test/java/rewards/internal/account/AccountTests.java
new file mode 100644
index 0000000..4075654
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/internal/account/AccountTests.java
@@ -0,0 +1,57 @@
+package rewards.internal.account;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for the Account class that verify Account behavior works in isolation.
+ */
+public class AccountTests {
+
+ private final Account account = new Account("1", "Keith and Keri Donald");
+
+ @Test
+ public void accountIsValid() {
+ // setup account with a valid set of beneficiaries to prepare for testing
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ assertTrue(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWithNoBeneficiaries() {
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreOver100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreUnder100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("25%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void makeContribution() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ assertEquals(contribution.getAmount(), MonetaryAmount.valueOf("100.00"));
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java b/lab/26-jdbc/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
new file mode 100644
index 0000000..b1615ad
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
@@ -0,0 +1,95 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC account repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcAccountRepositoryTests {
+
+ private JdbcAccountRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ repository = new JdbcAccountRepository(dataSource);
+ }
+
+ @Test
+ public void testFindAccountByCreditCard() {
+ Account account = repository.findByCreditCard("1234123412341234");
+ // assert the returned account contains what you expect given the state of the database
+ assertNotNull(account, "account should never be null");
+ assertEquals(Long.valueOf(0), account.getEntityId(), "wrong entity id");
+ assertEquals("123456789", account.getNumber(), "wrong account number");
+ assertEquals("Keith and Keri Donald", account.getName(), "wrong name");
+ assertEquals(2, account.getBeneficiaries().size(), "wrong beneficiary collection size");
+
+ Beneficiary b1 = account.getBeneficiary("Annabelle");
+ assertNotNull(b1, "Annabelle should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b1.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b1.getAllocationPercentage(), "wrong allocation percentage");
+
+ Beneficiary b2 = account.getBeneficiary("Corgan");
+ assertNotNull(b2, "Corgan should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b2.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b2.getAllocationPercentage(), "wrong allocation percentage");
+ }
+
+ @Test
+ public void testFindAccountByCreditCardNoAccount() {
+ assertThrows(EmptyResultDataAccessException.class, () -> {
+ repository.findByCreditCard("bogus");
+ });
+ }
+
+ @Test
+ public void testUpdateBeneficiaries() throws SQLException {
+ Account account = repository.findByCreditCard("1234123412341234");
+ account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ repository.updateBeneficiaries(account);
+ verifyBeneficiaryTableUpdated();
+ }
+
+ private void verifyBeneficiaryTableUpdated() throws SQLException {
+ String sql = "select SAVINGS from T_ACCOUNT_BENEFICIARY where NAME = ? and ACCOUNT_ID = ?";
+ PreparedStatement stmt = dataSource.getConnection().prepareStatement(sql);
+
+ // assert Annabelle has $4.00 savings now
+ stmt.setString(1, "Annabelle");
+ stmt.setLong(2, 0L);
+ ResultSet rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+
+ // assert Corgan has $4.00 savings now
+ stmt.setString(1, "Corgan");
+ stmt.setLong(2, 0L);
+ rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/26-jdbc/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java b/lab/26-jdbc/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
new file mode 100644
index 0000000..06ca758
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
@@ -0,0 +1,51 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC restaurant repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRestaurantRepositoryTests {
+
+ private JdbcRestaurantRepository repository;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRestaurantRepository(createTestDataSource());
+ }
+
+ @Test
+ public void testFindRestaurantByMerchantNumber() {
+ Restaurant restaurant = repository.findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "the restaurant should never be null");
+ assertEquals("1234567890", restaurant.getNumber(), "the merchant number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "the name is wrong");
+ assertEquals(Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "the benefitPercentage is wrong");
+ assertEquals(JdbcRestaurantRepository.AlwaysAvailable.INSTANCE,
+ restaurant.getBenefitAvailabilityPolicy(), "the benefit availability policy is wrong");
+ }
+
+ @Test
+ public void testFindRestaurantByBogusMerchantNumber() {
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("bogus");
+ });
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/26-jdbc/src/test/java/rewards/internal/restaurant/RestaurantTests.java b/lab/26-jdbc/src/test/java/rewards/internal/restaurant/RestaurantTests.java
new file mode 100644
index 0000000..93c2496
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/internal/restaurant/RestaurantTests.java
@@ -0,0 +1,71 @@
+package rewards.internal.restaurant;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for exercising the behavior of the Restaurant aggregate entity. A restaurant calculates a benefit to award
+ * to an account for dining based on an availability policy and benefit percentage.
+ */
+public class RestaurantTests {
+
+ private Restaurant restaurant;
+
+ private Account account;
+
+ private Dining dining;
+
+ @BeforeEach
+ public void setUp() {
+ // configure the restaurant, the object being tested
+ restaurant = new Restaurant("1234567890", "AppleBee's");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(true));
+ // configure supporting objects needed by the restaurant
+ account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle");
+ dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+ }
+
+ @Test
+ public void testCalcuateBenefitFor() {
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert 8.00 eligible for reward
+ assertEquals(MonetaryAmount.valueOf("8.00"), benefit);
+ }
+
+ @Test
+ public void testNoBenefitAvailable() {
+ // configure stub that always returns false
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(false));
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert zero eligible for reward
+ assertEquals(MonetaryAmount.valueOf("0.00"), benefit);
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy containing a single flag used to determine if benefit is available.
+ * Only useful for testing--a real availability policy might consider many factors such as the day of week of the
+ * dining, or the account's reward history for the current month.
+ */
+ private static class StubBenefitAvailibilityPolicy implements BenefitAvailabilityPolicy {
+
+ private final boolean isBenefitAvailable;
+
+ public StubBenefitAvailibilityPolicy(boolean isBenefitAvailable) {
+ this.isBenefitAvailable = isBenefitAvailable;
+ }
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return isBenefitAvailable;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/26-jdbc/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java b/lab/26-jdbc/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
new file mode 100644
index 0000000..46f8be8
--- /dev/null
+++ b/lab/26-jdbc/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
@@ -0,0 +1,105 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.Account;
+
+import javax.sql.DataSource;
+import java.math.BigDecimal;
+import java.sql.SQLException;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Tests the JDBC reward repository with a test data source to verify
+ * data access and relational-to-object mapping behavior works as expected.
+ *
+ * TODO-00: In this lab, you are going to exercise the following:
+ * - Refactoring cumbersome low-level JDBC code to leverage Spring's JdbcTemplate
+ * - Using various query methods of JdbcTemplate for retrieving data
+ * - Implementing callbacks for converting retrieved data into domain object
+ * - RowMapper
+ * - ResultSetExtractor (optional)
+ */
+public class JdbcRewardRepositoryTests {
+
+ private JdbcRewardRepository repository;
+
+ private DataSource dataSource;
+
+ private JdbcTemplate jdbcTemplate;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ repository = new JdbcRewardRepository(dataSource);
+ jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ @Test
+ public void testCreateReward() throws SQLException {
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "0123456789");
+
+ Account account = new Account("1", "Keith and Keri Donald");
+ account.setEntityId(0L);
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ RewardConfirmation confirmation = repository.confirmReward(contribution, dining);
+ assertNotNull(confirmation, "confirmation should not be null");
+ assertNotNull(confirmation.getConfirmationNumber(), "confirmation number should not be null");
+ assertEquals(contribution, confirmation.getAccountContribution(), "wrong contribution object");
+ verifyRewardInserted(confirmation, dining);
+ }
+
+ private void verifyRewardInserted(RewardConfirmation confirmation, Dining dining) {
+ assertEquals(1, getRewardCount());
+
+ // TODO-02: Use JdbcTemplate to query for a map of all column values
+ // of a row in the T_REWARD table based on the confirmationNumber.
+ // - Use "SELECT * FROM T_REWARD WHERE CONFIRMATION_NUMBER = ?" as SQL statement
+ // - After making the changes, execute this test class to verify
+ // its successful execution.
+ // (If you are using Gradle, comment out the test exclude in
+ // the build.gradle file.)
+ //
+
+ Map values = null;
+ verifyInsertedValues(confirmation, dining, values);
+ }
+
+ private void verifyInsertedValues(RewardConfirmation confirmation, Dining dining, Map values) {
+ assertEquals(confirmation.getAccountContribution().getAmount(), new MonetaryAmount((BigDecimal) values
+ .get("REWARD_AMOUNT")));
+ assertEquals(SimpleDate.today().asDate(), values.get("REWARD_DATE"));
+ assertEquals(confirmation.getAccountContribution().getAccountNumber(), values.get("ACCOUNT_NUMBER"));
+ assertEquals(dining.getAmount(), new MonetaryAmount((BigDecimal) values.get("DINING_AMOUNT")));
+ assertEquals(dining.getMerchantNumber(), values.get("DINING_MERCHANT_NUMBER"));
+ assertEquals(SimpleDate.today().asDate(), values.get("DINING_DATE"));
+ }
+
+ private int getRewardCount() {
+ // TODO-01: Use JdbcTemplate to query for the number of rows in the T_REWARD table
+ // - Use "SELECT count(*) FROM T_REWARD" as SQL statement
+ return -1;
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/26-jdbc/src/test/resources/rewards/testdb/data.sql b/lab/26-jdbc/src/test/resources/rewards/testdb/data.sql
new file mode 100644
index 0000000..28a87cc
--- /dev/null
+++ b/lab/26-jdbc/src/test/resources/rewards/testdb/data.sql
@@ -0,0 +1,78 @@
+
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456789', 'Keith and Keri Donald');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456001', 'Dollie R. Adams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456002', 'Cornelia J. Andresen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456003', 'Coral Villareal Betancourt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456004', 'Chad I. Cobbs');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456005', 'Michael C. Feller');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456006', 'Michael J. Grover');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456007', 'John C. Howard');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456008', 'Ida Ketterer');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456009', 'Laina Ochoa Lucero');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456010', 'Wesley M. Mayo');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456011', 'Leslie F. Mcclary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456012', 'John D. Mudra');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456013', 'Pietronella J. Nielsen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456014', 'John S. Oleary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456015', 'Glenda D. Smith');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456016', 'Willemina O. Thygesen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456017', 'Antje Vogt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456018', 'Julia Weber');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456019', 'Mark T. Williams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456020', 'Christine J. Wilson');
+
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (0, '1234123412341234');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (1, '1234123412340001');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (2, '1234123412340002');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (3, '1234123412340003');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (4, '1234123412340004');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (5, '1234123412340005');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (6, '1234123412340006');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (7, '1234123412340007');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (8, '1234123412340008');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (9, '1234123412340009');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (10, '1234123412340010');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (11, '1234123412340011');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (12, '1234123412340012');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (13, '1234123412340013');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (14, '1234123412340014');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (15, '1234123412340015');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (16, '1234123412340016');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (17, '1234123412340017');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (18, '1234123412340018');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (19, '1234123412340019');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (20, '1234123412340020');
+
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (0, 'Annabelle', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (0, 'Corgan', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Antolin', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Argus', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Gian', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Argeo', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Kai', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Kasper', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Ernst', .34, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (12, 'Brian', .75, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (12, 'Shelby', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Charles', .50, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Thomas', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Neil', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (17, 'Daniel', 1.0, 0.00);
+
+insert into T_RESTAURANT (MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY)
+ values ('1234567890', 'AppleBees', .08, 'A');
diff --git a/lab/26-jdbc/src/test/resources/rewards/testdb/schema.sql b/lab/26-jdbc/src/test/resources/rewards/testdb/schema.sql
new file mode 100644
index 0000000..b0324fa
--- /dev/null
+++ b/lab/26-jdbc/src/test/resources/rewards/testdb/schema.sql
@@ -0,0 +1,20 @@
+drop table T_ACCOUNT_BENEFICIARY if exists;
+drop table T_ACCOUNT_CREDIT_CARD if exists;
+drop table T_ACCOUNT if exists;
+drop table T_RESTAURANT if exists;
+drop table T_REWARD if exists;
+drop sequence S_REWARD_CONFIRMATION_NUMBER if exists;
+drop table DUAL_REWARD_CONFIRMATION_NUMBER if exists;
+
+create table T_ACCOUNT (ID integer identity primary key, NUMBER varchar(9), NAME varchar(50) not null, unique(NUMBER));
+create table T_ACCOUNT_CREDIT_CARD (ID integer identity primary key, ACCOUNT_ID integer, NUMBER varchar(16), unique(ACCOUNT_ID, NUMBER));
+create table T_ACCOUNT_BENEFICIARY (ID integer identity primary key, ACCOUNT_ID integer, NAME varchar(50), ALLOCATION_PERCENTAGE decimal(3,2) not null, SAVINGS decimal(8,2) not null, unique(ACCOUNT_ID, NAME));
+create table T_RESTAURANT (ID integer identity primary key, MERCHANT_NUMBER varchar(10) not null, NAME varchar(80) not null, BENEFIT_PERCENTAGE decimal(3,2) not null, BENEFIT_AVAILABILITY_POLICY varchar(1) not null, unique(MERCHANT_NUMBER));
+create table T_REWARD (ID integer identity primary key, CONFIRMATION_NUMBER varchar(25) not null, REWARD_AMOUNT decimal(8,2) not null, REWARD_DATE date not null, ACCOUNT_NUMBER varchar(9) not null, DINING_AMOUNT decimal not null, DINING_MERCHANT_NUMBER varchar(10) not null, DINING_DATE date not null, unique(CONFIRMATION_NUMBER));
+
+create sequence S_REWARD_CONFIRMATION_NUMBER start with 1;
+create table DUAL_REWARD_CONFIRMATION_NUMBER (ZERO integer);
+insert into DUAL_REWARD_CONFIRMATION_NUMBER values (0);
+
+alter table T_ACCOUNT_CREDIT_CARD add constraint FK_ACCOUNT_CREDIT_CARD foreign key (ACCOUNT_ID) references T_ACCOUNT(ID) on delete cascade;
+alter table T_ACCOUNT_BENEFICIARY add constraint FK_ACCOUNT_BENEFICIARY foreign key (ACCOUNT_ID) references T_ACCOUNT(ID) on delete cascade;
\ No newline at end of file
diff --git a/lab/28-transactions-solution/build.gradle b/lab/28-transactions-solution/build.gradle
new file mode 100644
index 0000000..af9f0d4
--- /dev/null
+++ b/lab/28-transactions-solution/build.gradle
@@ -0,0 +1,3 @@
+dependencies {
+ implementation project(':00-rewards-common')
+}
diff --git a/lab/28-transactions-solution/pom.xml b/lab/28-transactions-solution/pom.xml
new file mode 100644
index 0000000..95fdf88
--- /dev/null
+++ b/lab/28-transactions-solution/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+ 28-transactions-solution
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
diff --git a/lab/28-transactions-solution/src/main/java/config/RewardsConfig.java b/lab/28-transactions-solution/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..d311007
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,56 @@
+package config;
+
+import javax.sql.DataSource;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.transaction.annotation.EnableTransactionManagement;
+
+import rewards.RewardNetwork;
+import rewards.internal.RewardNetworkImpl;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.account.JdbcAccountRepository;
+import rewards.internal.restaurant.JdbcRestaurantRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.JdbcRewardRepository;
+import rewards.internal.reward.RewardRepository;
+
+
+@Configuration
+@EnableTransactionManagement
+public class RewardsConfig {
+
+ @Autowired
+ DataSource dataSource;
+
+ @Bean
+ public RewardNetwork rewardNetwork(){
+ return new RewardNetworkImpl(
+ accountRepository(),
+ restaurantRepository(),
+ rewardRepository());
+ }
+
+ @Bean
+ public AccountRepository accountRepository(){
+ JdbcAccountRepository repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RestaurantRepository restaurantRepository(){
+ JdbcRestaurantRepository repository = new JdbcRestaurantRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RewardRepository rewardRepository(){
+ JdbcRewardRepository repository = new JdbcRewardRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+}
diff --git a/lab/28-transactions-solution/src/main/java/rewards/AccountContribution.java b/lab/28-transactions-solution/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/Dining.java b/lab/28-transactions-solution/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/RewardConfirmation.java b/lab/28-transactions-solution/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/RewardNetwork.java b/lab/28-transactions-solution/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/28-transactions-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..8de8f12
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,55 @@
+package rewards.internal;
+
+import org.springframework.transaction.annotation.Transactional;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ @Transactional
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/RewardNetworkImplRequiresNew.java b/lab/28-transactions-solution/src/main/java/rewards/internal/RewardNetworkImplRequiresNew.java
new file mode 100644
index 0000000..3433b81
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/RewardNetworkImplRequiresNew.java
@@ -0,0 +1,56 @@
+package rewards.internal;
+
+import org.springframework.transaction.annotation.Propagation;
+import org.springframework.transaction.annotation.Transactional;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+public class RewardNetworkImplRequiresNew implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImplRequiresNew(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ @Transactional(propagation=Propagation.REQUIRES_NEW)
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/account/Account.java b/lab/28-transactions-solution/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..84463f0
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,159 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ try {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ } catch (IllegalArgumentException e) {
+ // total would have been over 100% - return invalid
+ return false;
+ }
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account. Callers should not attempt to hold on or modify the returned set.
+ * This method should only be used transitively; for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Returns a single account beneficiary. Callers should not attempt to hold on or modify the returned object. This
+ * method should only be used transitively; for example, called to facilitate reporting or testing.
+ * @param name the name of the beneficiary e.g "Annabelle"
+ * @return the beneficiary object
+ */
+ public Beneficiary getBeneficiary(String name) {
+ for (Beneficiary b : beneficiaries) {
+ if (b.getName().equals(name)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("No such beneficiary with name '" + name + "'");
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/account/AccountRepository.java b/lab/28-transactions-solution/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/account/Beneficiary.java b/lab/28-transactions-solution/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/28-transactions-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..e1ddf17
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,92 @@
+package rewards.internal.account;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.ResultSetExtractor;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+public class JdbcAccountRepository implements AccountRepository {
+
+ private JdbcTemplate jdbcTemplate;
+
+ /**
+ * Extracts an Account object from rows returned from a join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY.
+ */
+ private final ResultSetExtractor accountExtractor = new AccountExtractor();
+
+ public void setDataSource(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = "select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?";
+ return jdbcTemplate.query(sql, accountExtractor, creditCardNumber);
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ for (Beneficiary b : account.getBeneficiaries()) {
+ jdbcTemplate.update(sql, b.getSavings().asBigDecimal(), account.getEntityId(), b.getName());
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ *
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+
+ private class AccountExtractor implements ResultSetExtractor {
+
+ public Account extractData(ResultSet rs) throws SQLException, DataAccessException {
+ return mapAccount(rs);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/account/package.html b/lab/28-transactions-solution/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java b/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
new file mode 100644
index 0000000..b7d6d74
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
@@ -0,0 +1,20 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * Determines if benefit is available for an account for dining.
+ *
+ * A value object. A strategy. Scoped by the Resturant aggregate.
+ */
+public interface BenefitAvailabilityPolicy {
+
+ /**
+ * Calculates if an account is eligible to receive benefits for a dining.
+ * @param account the account of the member who dined
+ * @param dining the dining event
+ * @return benefit availability status
+ */
+ boolean isBenefitAvailableFor(Account account, Dining dining);
+}
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..f4cff67
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,117 @@
+package rewards.internal.restaurant;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.Percentage;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ */
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private JdbcTemplate jdbcTemplate;
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ */
+ private final RowMapper rowMapper = new RestaurantRowMapper();
+
+ public void setDataSource(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ String sql = "select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY from T_RESTAURANT where MERCHANT_NUMBER = ?";
+ return jdbcTemplate.queryForObject(sql, rowMapper, merchantNumber);
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+ // map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ restaurant.setBenefitAvailabilityPolicy(mapBenefitAvailabilityPolicy(rs));
+ return restaurant;
+ }
+
+ /**
+ * Helper method that maps benefit availability policy data in the ResultSet to a fully-configured
+ * {@link BenefitAvailabilityPolicy} object. The key column is 'BENEFIT_AVAILABILITY_POLICY', which is a
+ * discriminator column containing a string code that identifies the type of policy. Currently supported types are:
+ * 'A' for 'always available' and 'N' for 'never available'.
+ *
+ *
+ * More types could be added easily by enhancing this method. For example, 'W' for 'Weekdays only' or 'M' for 'Max
+ * Rewards per Month'. Some of these types might require additional database column values to be configured, for
+ * example a 'MAX_REWARDS_PER_MONTH' data column.
+ *
+ * @param rs the result set used to map the policy object from database column values
+ * @return the matching benefit availability policy
+ * @throws IllegalArgumentException if the mapping could not be performed
+ */
+ private BenefitAvailabilityPolicy mapBenefitAvailabilityPolicy(ResultSet rs) throws SQLException {
+ String policyCode = rs.getString("BENEFIT_AVAILABILITY_POLICY");
+ if ("A".equals(policyCode)) {
+ return AlwaysAvailable.INSTANCE;
+ } else if ("N".equals(policyCode)) {
+ return NeverAvailable.INSTANCE;
+ } else {
+ throw new IllegalArgumentException("Not a supported policy code " + policyCode);
+ }
+ }
+
+ /**
+ * Returns true indicating benefit is always available.
+ */
+ static class AlwaysAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new AlwaysAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+
+ public String toString() {
+ return "alwaysAvailable";
+ }
+ }
+
+ /**
+ * Returns false indicating benefit is never available.
+ */
+ static class NeverAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new NeverAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return false;
+ }
+
+ public String toString() {
+ return "neverAvailable";
+ }
+ }
+
+ private class RestaurantRowMapper implements RowMapper {
+
+ public Restaurant mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return mapRestaurant(rs);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..2e73e57
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,102 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on an availability policy and a
+ * benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ private BenefitAvailabilityPolicy benefitAvailabilityPolicy;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Sets the policy that determines if a dining by an account at this restaurant is eligible for benefit.
+ * @param benefitAvailabilityPolicy the benefit availability policy
+ */
+ public void setBenefitAvailabilityPolicy(BenefitAvailabilityPolicy benefitAvailabilityPolicy) {
+ this.benefitAvailabilityPolicy = benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Returns this restaurant's benefit availability policy.
+ */
+ public BenefitAvailabilityPolicy getBenefitAvailabilityPolicy() {
+ return benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ if (benefitAvailabilityPolicy.isBenefitAvailableFor(account, dining)) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ } else {
+ return MonetaryAmount.zero();
+ }
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage
+ + ", benefitAvailabilityPolicy = " + benefitAvailabilityPolicy;
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/package.html b/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/28-transactions-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..e5ae22b
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,38 @@
+package rewards.internal.reward;
+
+import javax.sql.DataSource;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+import common.datetime.SimpleDate;
+
+/**
+ * JDBC implementation of a reward repository that records the result of a reward transaction by inserting a reward
+ * confirmation record.
+ */
+public class JdbcRewardRepository implements RewardRepository {
+
+ private JdbcTemplate jdbcTemplate;
+
+ public void setDataSource(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ String confirmationNumber = nextConfirmationNumber();
+ jdbcTemplate.update(sql, confirmationNumber, contribution.getAmount().asBigDecimal(),
+ SimpleDate.today().asDate(), contribution.getAccountNumber(), dining.getMerchantNumber(),
+ dining.getDate().asDate(), dining.getAmount().asBigDecimal());
+ return new RewardConfirmation(confirmationNumber, contribution);
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ return jdbcTemplate.queryForObject(sql, String.class);
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/28-transactions-solution/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..1207f0f
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/main/java/rewards/internal/reward/package.html b/lab/28-transactions-solution/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/28-transactions-solution/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/28-transactions-solution/src/test/java/rewards/RewardNetworkPropagationTests.java b/lab/28-transactions-solution/src/test/java/rewards/RewardNetworkPropagationTests.java
new file mode 100644
index 0000000..a5db0ef
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/RewardNetworkPropagationTests.java
@@ -0,0 +1,57 @@
+package rewards;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.transaction.PlatformTransactionManager;
+import org.springframework.transaction.TransactionStatus;
+import org.springframework.transaction.support.DefaultTransactionDefinition;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A system test that demonstrates how propagation settings affect transactional execution.
+ */
+@SpringJUnitConfig(classes = {SystemTestRequiresNewConfig.class})
+public class RewardNetworkPropagationTests {
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ /**
+ * A template to use for test verification
+ */
+ private JdbcTemplate template;
+
+ /**
+ * Manages transaction manually
+ */
+ @Autowired
+ private PlatformTransactionManager transactionManager;
+
+ @Autowired
+ public void initJdbcTemplate(DataSource dataSource) {
+ this.template = new JdbcTemplate(dataSource);
+ }
+
+ @Test
+ public void testPropagation() {
+ // Open a transaction for testing
+ TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+ rewardNetwork.rewardAccountFor(dining);
+
+ // Rollback the transaction test transaction
+ transactionManager.rollback(status);
+
+ String sql = "select SAVINGS from T_ACCOUNT_BENEFICIARY where NAME = ?";
+ assertEquals(Double.valueOf(4.00), template.queryForObject(sql, Double.class, "Annabelle"));
+ assertEquals(Double.valueOf(4.00), template.queryForObject(sql, Double.class, "Corgan"));
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/test/java/rewards/RewardNetworkSideEffectTests.java b/lab/28-transactions-solution/src/test/java/rewards/RewardNetworkSideEffectTests.java
new file mode 100644
index 0000000..34aa86d
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/RewardNetworkSideEffectTests.java
@@ -0,0 +1,87 @@
+package rewards;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * A system test that demonstrates how the effects of a given test can affect
+ * all tests that follow. JUnit makes no guarantee about the order that tests
+ * run in, so we force tests to run in method name order.
+ */
+@SpringJUnitConfig(classes = {SystemTestConfig.class})
+@Transactional
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class RewardNetworkSideEffectTests {
+
+ private static final String SAVINGS_SQL = "select SAVINGS from T_ACCOUNT_BENEFICIARY where NAME = ?";
+
+ /**
+ * Amount of money in Annabelle's savings account before running the test
+ * methods
+ */
+ private static Double annabelleInitialSavings;
+
+ /**
+ * Amount of money in Corgan's savings account before running the test
+ * methods
+ */
+ private static Double corganInitialSavings;
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ /**
+ * A template to use for test verification
+ */
+ private JdbcTemplate jdbcTemplate;
+
+ @Autowired
+ public void initJdbcTemplate(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ /**
+ * Determine the initial savings: note that we're doing this only once for
+ * all tests, so if a test changes the savings and commits the next test
+ * method might be affected!
+ */
+ @BeforeEach
+ public void determineInitialSavings() {
+ if (annabelleInitialSavings == null) {
+ annabelleInitialSavings = jdbcTemplate.queryForObject(SAVINGS_SQL, Double.class, "Annabelle");
+ corganInitialSavings = jdbcTemplate.queryForObject(SAVINGS_SQL, Double.class, "Corgan");
+ }
+ }
+
+ private void runTest() {
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+ rewardNetwork.rewardAccountFor(dining);
+ assertEquals(Double.valueOf(annabelleInitialSavings + 4.00d),
+ jdbcTemplate.queryForObject(SAVINGS_SQL, Double.class, "Annabelle"));
+ assertEquals(Double.valueOf(corganInitialSavings + 4.00d),
+ jdbcTemplate.queryForObject(SAVINGS_SQL, Double.class, "Corgan"));
+ }
+
+ @Test
+ public void testCollision1stTime() {
+ runTest();
+ }
+
+ @Test
+ public void testCollision2ndTime() {
+ runTest();
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/test/java/rewards/RewardNetworkTests.java b/lab/28-transactions-solution/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..656c0b5
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,86 @@
+package rewards;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application
+ * work together to reward for dining successfully. Uses Spring to bootstrap the
+ * application for use in a test environment.
+ */
+@SpringJUnitConfig(classes = {SystemTestConfig.class})
+public class RewardNetworkTests {
+
+ /**
+ * The test entry point:
+ */
+ @Autowired
+ RewardNetwork rewardNetwork;
+
+ @Autowired
+ DataSource dataSource;
+
+ JdbcTemplate jdbcTemplate;
+
+ @BeforeEach
+ public void setup() {
+ jdbcTemplate = new JdbcTemplate(dataSource);
+
+ // Using Logback for logging.
+ // Enable DEBUG logging so we can see the transactions
+ Logger jdbcLogger = (Logger) LoggerFactory
+ .getLogger("org.springframework.jdbc.datasource.DataSourceTransactionManager");
+ jdbcLogger.setLevel(Level.DEBUG);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card
+ // '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // Check the DB to see if the Reward is actually on the table:
+ String sql = "SELECT COUNT(*) FROM T_REWARD WHERE CONFIRMATION_NUMBER = ? AND REWARD_AMOUNT = ?";
+ int count = jdbcTemplate.queryForObject(sql, Integer.class, confirmation.getConfirmationNumber(),
+ confirmation.getAccountContribution().getAmount().asBigDecimal());
+
+ assertEquals(1, count);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2
+ // distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/test/java/rewards/SystemTestConfig.java b/lab/28-transactions-solution/src/test/java/rewards/SystemTestConfig.java
new file mode 100644
index 0000000..d3c3461
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/SystemTestConfig.java
@@ -0,0 +1,39 @@
+package rewards;
+
+import javax.sql.DataSource;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.jdbc.datasource.DataSourceTransactionManager;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import org.springframework.transaction.PlatformTransactionManager;
+
+import config.RewardsConfig;
+
+
+@Configuration
+@Import(RewardsConfig.class)
+public class SystemTestConfig {
+
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+ @Bean
+ public DataSource dataSource(){
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:rewards/testdb/schema.sql")
+ .addScript("classpath:rewards/testdb/data.sql")
+ .build();
+ }
+
+
+ @Bean
+ public PlatformTransactionManager transactionManager(){
+ return new DataSourceTransactionManager(dataSource());
+ }
+
+}
diff --git a/lab/28-transactions-solution/src/test/java/rewards/SystemTestRequiresNewConfig.java b/lab/28-transactions-solution/src/test/java/rewards/SystemTestRequiresNewConfig.java
new file mode 100644
index 0000000..75a8b6b
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/SystemTestRequiresNewConfig.java
@@ -0,0 +1,28 @@
+package rewards;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+import rewards.internal.RewardNetworkImplRequiresNew;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+
+@Configuration
+@Import(SystemTestConfig.class)
+public class SystemTestRequiresNewConfig {
+
+ @Bean
+ public RewardNetwork rewardNetwork(
+ AccountRepository accountRepository,
+ RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository ) {
+ return new RewardNetworkImplRequiresNew(
+ accountRepository,
+ restaurantRepository,
+ rewardRepository);
+ }
+
+}
diff --git a/lab/28-transactions-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/28-transactions-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..98b7353
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,72 @@
+package rewards.internal;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/test/java/rewards/internal/StubAccountRepository.java b/lab/28-transactions-solution/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..b926be2
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,43 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/28-transactions-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..418516d
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,53 @@
+package rewards.internal;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import org.springframework.dao.EmptyResultDataAccessException;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+import rewards.internal.restaurant.BenefitAvailabilityPolicy;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import common.money.Percentage;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new AlwaysReturnsTrue());
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy that always returns true. Only useful for testing--a real
+ * availability policy might consider many factors such as the day of week of the dining, or the account's reward
+ * history for the current month.
+ */
+ private static class AlwaysReturnsTrue implements BenefitAvailabilityPolicy {
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/test/java/rewards/internal/StubRewardRepository.java b/lab/28-transactions-solution/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..2487aca
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import java.util.Random;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/test/java/rewards/internal/account/AccountTests.java b/lab/28-transactions-solution/src/test/java/rewards/internal/account/AccountTests.java
new file mode 100644
index 0000000..4075654
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/internal/account/AccountTests.java
@@ -0,0 +1,57 @@
+package rewards.internal.account;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import rewards.AccountContribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for the Account class that verify Account behavior works in isolation.
+ */
+public class AccountTests {
+
+ private final Account account = new Account("1", "Keith and Keri Donald");
+
+ @Test
+ public void accountIsValid() {
+ // setup account with a valid set of beneficiaries to prepare for testing
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ assertTrue(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWithNoBeneficiaries() {
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreOver100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreUnder100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("25%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void makeContribution() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ assertEquals(contribution.getAmount(), MonetaryAmount.valueOf("100.00"));
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java b/lab/28-transactions-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
new file mode 100644
index 0000000..4326571
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
@@ -0,0 +1,96 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC account repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcAccountRepositoryTests {
+
+ private JdbcAccountRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ }
+
+ @Test
+ public void testFindAccountByCreditCard() {
+ Account account = repository.findByCreditCard("1234123412341234");
+ // assert the returned account contains what you expect given the state of the database
+ assertNotNull(account, "account should never be null");
+ assertEquals(Long.valueOf(0), account.getEntityId(), "wrong entity id");
+ assertEquals("123456789", account.getNumber(), "wrong account number");
+ assertEquals("Keith and Keri Donald", account.getName(), "wrong name");
+ assertEquals(2, account.getBeneficiaries().size(), "wrong beneficiary collection size");
+
+ Beneficiary b1 = account.getBeneficiary("Annabelle");
+ assertNotNull(b1, "Annabelle should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b1.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b1.getAllocationPercentage(), "wrong allocation percentage");
+
+ Beneficiary b2 = account.getBeneficiary("Corgan");
+ assertNotNull(b2, "Corgan should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b2.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b2.getAllocationPercentage(), "wrong allocation percentage");
+ }
+
+ @Test
+ public void testFindAccountByCreditCardNoAccount() {
+ assertThrows(EmptyResultDataAccessException.class, () -> {
+ repository.findByCreditCard("bogus");
+ });
+ }
+
+ @Test
+ public void testUpdateBeneficiaries() throws SQLException {
+ Account account = repository.findByCreditCard("1234123412341234");
+ account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ repository.updateBeneficiaries(account);
+ verifyBeneficiaryTableUpdated();
+ }
+
+ private void verifyBeneficiaryTableUpdated() throws SQLException {
+ String sql = "select SAVINGS from T_ACCOUNT_BENEFICIARY where NAME = ? and ACCOUNT_ID = ?";
+ PreparedStatement stmt = dataSource.getConnection().prepareStatement(sql);
+
+ // assert Annabelle has $4.00 savings now
+ stmt.setString(1, "Annabelle");
+ stmt.setLong(2, 0L);
+ ResultSet rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+
+ // assert Corgan has $4.00 savings now
+ stmt.setString(1, "Corgan");
+ stmt.setLong(2, 0L);
+ rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/28-transactions-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java b/lab/28-transactions-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
new file mode 100644
index 0000000..565dc1b
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
@@ -0,0 +1,52 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC restaurant repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRestaurantRepositoryTests {
+
+ private JdbcRestaurantRepository repository;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRestaurantRepository();
+ repository.setDataSource(createTestDataSource());
+ }
+
+ @Test
+ public void testFindRestaurantByMerchantNumber() {
+ Restaurant restaurant = repository.findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "the restaurant should never be null");
+ assertEquals("1234567890", restaurant.getNumber(), "the merchant number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "the name is wrong");
+ assertEquals(Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "the benefitPercentage is wrong");
+ assertEquals(JdbcRestaurantRepository.AlwaysAvailable.INSTANCE,
+ restaurant.getBenefitAvailabilityPolicy(), "the benefit availability policy is wrong");
+ }
+
+ @Test
+ public void testFindRestaurantByBogusMerchantNumber() {
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("bogus");
+ });
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/28-transactions-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java b/lab/28-transactions-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java
new file mode 100644
index 0000000..93c2496
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java
@@ -0,0 +1,71 @@
+package rewards.internal.restaurant;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Unit tests for exercising the behavior of the Restaurant aggregate entity. A restaurant calculates a benefit to award
+ * to an account for dining based on an availability policy and benefit percentage.
+ */
+public class RestaurantTests {
+
+ private Restaurant restaurant;
+
+ private Account account;
+
+ private Dining dining;
+
+ @BeforeEach
+ public void setUp() {
+ // configure the restaurant, the object being tested
+ restaurant = new Restaurant("1234567890", "AppleBee's");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(true));
+ // configure supporting objects needed by the restaurant
+ account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle");
+ dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+ }
+
+ @Test
+ public void testCalcuateBenefitFor() {
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert 8.00 eligible for reward
+ assertEquals(MonetaryAmount.valueOf("8.00"), benefit);
+ }
+
+ @Test
+ public void testNoBenefitAvailable() {
+ // configure stub that always returns false
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(false));
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert zero eligible for reward
+ assertEquals(MonetaryAmount.valueOf("0.00"), benefit);
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy containing a single flag used to determine if benefit is available.
+ * Only useful for testing--a real availability policy might consider many factors such as the day of week of the
+ * dining, or the account's reward history for the current month.
+ */
+ private static class StubBenefitAvailibilityPolicy implements BenefitAvailabilityPolicy {
+
+ private final boolean isBenefitAvailable;
+
+ public StubBenefitAvailibilityPolicy(boolean isBenefitAvailable) {
+ this.isBenefitAvailable = isBenefitAvailable;
+ }
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return isBenefitAvailable;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java b/lab/28-transactions-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
new file mode 100644
index 0000000..5af5f02
--- /dev/null
+++ b/lab/28-transactions-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
@@ -0,0 +1,92 @@
+package rewards.internal.reward;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.math.BigDecimal;
+import java.sql.SQLException;
+import java.util.Map;
+
+import javax.sql.DataSource;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.Account;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Tests the JDBC reward repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRewardRepositoryTests {
+
+ private JdbcRewardRepository repository;
+
+ private DataSource dataSource;
+
+ private JdbcTemplate jdbcTemplate;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRewardRepository();
+ dataSource = createTestDataSource();
+ repository.setDataSource(dataSource);
+ jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ @Test
+ public void testCreateReward() throws SQLException {
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "0123456789");
+
+ Account account = new Account("1", "Keith and Keri Donald");
+ account.setEntityId(0L);
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ RewardConfirmation confirmation = repository.confirmReward(contribution, dining);
+ assertNotNull(confirmation, "confirmation should not be null");
+ assertNotNull(confirmation.getConfirmationNumber(), "confirmation number should not be null");
+ assertEquals(contribution, confirmation.getAccountContribution(), "wrong contribution object");
+ verifyRewardInserted(confirmation, dining);
+ }
+
+ private void verifyRewardInserted(RewardConfirmation confirmation, Dining dining) {
+ assertEquals(1, getRewardCount());
+ String sql = "select * from T_REWARD where CONFIRMATION_NUMBER = ?";
+ Map values = jdbcTemplate.queryForMap(sql, confirmation.getConfirmationNumber());
+ verifyInsertedValues(confirmation, dining, values);
+ }
+
+ private void verifyInsertedValues(RewardConfirmation confirmation, Dining dining, Map values) {
+ assertEquals(confirmation.getAccountContribution().getAmount(), new MonetaryAmount((BigDecimal) values
+ .get("REWARD_AMOUNT")));
+ assertEquals(SimpleDate.today().asDate(), values.get("REWARD_DATE"));
+ assertEquals(confirmation.getAccountContribution().getAccountNumber(), values.get("ACCOUNT_NUMBER"));
+ assertEquals(dining.getAmount(), new MonetaryAmount((BigDecimal) values.get("DINING_AMOUNT")));
+ assertEquals(dining.getMerchantNumber(), values.get("DINING_MERCHANT_NUMBER"));
+ assertEquals(SimpleDate.today().asDate(), values.get("DINING_DATE"));
+ }
+
+ private int getRewardCount() {
+ String sql = "select count(*) from T_REWARD";
+ return jdbcTemplate.queryForObject(sql, Integer.class);
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/28-transactions/build.gradle b/lab/28-transactions/build.gradle
new file mode 100644
index 0000000..2618ecd
--- /dev/null
+++ b/lab/28-transactions/build.gradle
@@ -0,0 +1,10 @@
+dependencies {
+ implementation project(':00-rewards-common')
+}
+
+test {
+ exclude '**/RewardNetworkTests.class'
+ exclude '**/RewardNetworkPropagationTests.class'
+ exclude '**/RewardNetworkSideEffectTests.class'
+}
+
diff --git a/lab/28-transactions/pom.xml b/lab/28-transactions/pom.xml
new file mode 100644
index 0000000..3414bbb
--- /dev/null
+++ b/lab/28-transactions/pom.xml
@@ -0,0 +1,37 @@
+
+
+ 4.0.0
+ 28-transactions
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ **/RewardNetworkTests.java
+ **/RewardNetworkSideEffectTests.java
+ **/RewardNetworkPropagationTests.java
+
+
+
+
+
+
diff --git a/lab/28-transactions/src/main/java/config/RewardsConfig.java b/lab/28-transactions/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..99cdf2f
--- /dev/null
+++ b/lab/28-transactions/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,55 @@
+package config;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import rewards.RewardNetwork;
+import rewards.internal.RewardNetworkImpl;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.account.JdbcAccountRepository;
+import rewards.internal.restaurant.JdbcRestaurantRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.JdbcRewardRepository;
+import rewards.internal.reward.RewardRepository;
+
+import javax.sql.DataSource;
+
+
+// TODO-03: Add an annotation to enable Spring transaction
+
+@Configuration
+public class RewardsConfig {
+
+ @Autowired
+ DataSource dataSource;
+
+ @Bean
+ public RewardNetwork rewardNetwork(){
+ return new RewardNetworkImpl(
+ accountRepository(),
+ restaurantRepository(),
+ rewardRepository());
+ }
+
+ @Bean
+ public AccountRepository accountRepository(){
+ JdbcAccountRepository repository = new JdbcAccountRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RestaurantRepository restaurantRepository(){
+ JdbcRestaurantRepository repository = new JdbcRestaurantRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RewardRepository rewardRepository(){
+ JdbcRewardRepository repository = new JdbcRewardRepository();
+ repository.setDataSource(dataSource);
+ return repository;
+ }
+
+}
diff --git a/lab/28-transactions/src/main/java/rewards/AccountContribution.java b/lab/28-transactions/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..5cad191
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import java.util.Set;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/Dining.java b/lab/28-transactions/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/RewardConfirmation.java b/lab/28-transactions/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/RewardNetwork.java b/lab/28-transactions/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/28-transactions/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..b04f702
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,65 @@
+package rewards.internal;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import common.money.MonetaryAmount;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer
+ * service responsible for coordinating with the domain-layer to carry out
+ * the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ *
+ * TODO-00: In this lab, you are going to exercise the following:
+ * - Enabling Spring Transaction
+ * - Adding transactional behavior to a method
+ * - Exercising transaction propagation
+ * - Exercising transactional behavior in test
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ // TODO-06: Modify the transactional attributes of the rewardAccountFor() method below.
+ // Switch the propagation level to require a NEW transaction whenever invoked.
+
+ // TODO-01: Annotate this method as needing transactional behavior
+ // Make sure to use the annotation from Spring not from Jakarta EE.
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
diff --git a/lab/28-transactions/src/main/java/rewards/internal/account/Account.java b/lab/28-transactions/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..84463f0
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,159 @@
+package rewards.internal.account;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ try {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ } catch (IllegalArgumentException e) {
+ // total would have been over 100% - return invalid
+ return false;
+ }
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account. Callers should not attempt to hold on or modify the returned set.
+ * This method should only be used transitively; for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Returns a single account beneficiary. Callers should not attempt to hold on or modify the returned object. This
+ * method should only be used transitively; for example, called to facilitate reporting or testing.
+ * @param name the name of the beneficiary e.g "Annabelle"
+ * @return the beneficiary object
+ */
+ public Beneficiary getBeneficiary(String name) {
+ for (Beneficiary b : beneficiaries) {
+ if (b.getName().equals(name)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("No such beneficiary with name '" + name + "'");
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/internal/account/AccountRepository.java b/lab/28-transactions/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/internal/account/Beneficiary.java b/lab/28-transactions/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/28-transactions/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..6a186e3
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,92 @@
+package rewards.internal.account;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.ResultSetExtractor;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+public class JdbcAccountRepository implements AccountRepository {
+
+ private JdbcTemplate jdbcTemplate;
+
+ /**
+ * Extracts an Account object from rows returned from a join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY.
+ */
+ private final ResultSetExtractor accountExtractor = new AccountExtractor();
+
+ public void setDataSource(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = "select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?";
+ return jdbcTemplate.query(sql, accountExtractor, creditCardNumber);
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ for (Beneficiary b : account.getBeneficiaries()) {
+ jdbcTemplate.update(sql, b.getSavings().asBigDecimal(), account.getEntityId(), b.getName());
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ *
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+
+ private class AccountExtractor implements ResultSetExtractor {
+
+ public Account extractData(ResultSet rs) throws SQLException, DataAccessException {
+ return mapAccount(rs);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/internal/account/package.html b/lab/28-transactions/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/lab/28-transactions/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java b/lab/28-transactions/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
new file mode 100644
index 0000000..b7d6d74
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
@@ -0,0 +1,20 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * Determines if benefit is available for an account for dining.
+ *
+ * A value object. A strategy. Scoped by the Resturant aggregate.
+ */
+public interface BenefitAvailabilityPolicy {
+
+ /**
+ * Calculates if an account is eligible to receive benefits for a dining.
+ * @param account the account of the member who dined
+ * @param dining the dining event
+ * @return benefit availability status
+ */
+ boolean isBenefitAvailableFor(Account account, Dining dining);
+}
diff --git a/lab/28-transactions/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/28-transactions/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..f4cff67
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,117 @@
+package rewards.internal.restaurant;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import javax.sql.DataSource;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.Percentage;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ */
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private JdbcTemplate jdbcTemplate;
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ */
+ private final RowMapper rowMapper = new RestaurantRowMapper();
+
+ public void setDataSource(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ String sql = "select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY from T_RESTAURANT where MERCHANT_NUMBER = ?";
+ return jdbcTemplate.queryForObject(sql, rowMapper, merchantNumber);
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+ // map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ restaurant.setBenefitAvailabilityPolicy(mapBenefitAvailabilityPolicy(rs));
+ return restaurant;
+ }
+
+ /**
+ * Helper method that maps benefit availability policy data in the ResultSet to a fully-configured
+ * {@link BenefitAvailabilityPolicy} object. The key column is 'BENEFIT_AVAILABILITY_POLICY', which is a
+ * discriminator column containing a string code that identifies the type of policy. Currently supported types are:
+ * 'A' for 'always available' and 'N' for 'never available'.
+ *
+ *
+ * More types could be added easily by enhancing this method. For example, 'W' for 'Weekdays only' or 'M' for 'Max
+ * Rewards per Month'. Some of these types might require additional database column values to be configured, for
+ * example a 'MAX_REWARDS_PER_MONTH' data column.
+ *
+ * @param rs the result set used to map the policy object from database column values
+ * @return the matching benefit availability policy
+ * @throws IllegalArgumentException if the mapping could not be performed
+ */
+ private BenefitAvailabilityPolicy mapBenefitAvailabilityPolicy(ResultSet rs) throws SQLException {
+ String policyCode = rs.getString("BENEFIT_AVAILABILITY_POLICY");
+ if ("A".equals(policyCode)) {
+ return AlwaysAvailable.INSTANCE;
+ } else if ("N".equals(policyCode)) {
+ return NeverAvailable.INSTANCE;
+ } else {
+ throw new IllegalArgumentException("Not a supported policy code " + policyCode);
+ }
+ }
+
+ /**
+ * Returns true indicating benefit is always available.
+ */
+ static class AlwaysAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new AlwaysAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+
+ public String toString() {
+ return "alwaysAvailable";
+ }
+ }
+
+ /**
+ * Returns false indicating benefit is never available.
+ */
+ static class NeverAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new NeverAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return false;
+ }
+
+ public String toString() {
+ return "neverAvailable";
+ }
+ }
+
+ private class RestaurantRowMapper implements RowMapper {
+
+ public Restaurant mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return mapRestaurant(rs);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/28-transactions/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..2e73e57
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,102 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on an availability policy and a
+ * benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ private BenefitAvailabilityPolicy benefitAvailabilityPolicy;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Sets the policy that determines if a dining by an account at this restaurant is eligible for benefit.
+ * @param benefitAvailabilityPolicy the benefit availability policy
+ */
+ public void setBenefitAvailabilityPolicy(BenefitAvailabilityPolicy benefitAvailabilityPolicy) {
+ this.benefitAvailabilityPolicy = benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Returns this restaurant's benefit availability policy.
+ */
+ public BenefitAvailabilityPolicy getBenefitAvailabilityPolicy() {
+ return benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ if (benefitAvailabilityPolicy.isBenefitAvailableFor(account, dining)) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ } else {
+ return MonetaryAmount.zero();
+ }
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage
+ + ", benefitAvailabilityPolicy = " + benefitAvailabilityPolicy;
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/28-transactions/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/28-transactions/src/main/java/rewards/internal/restaurant/package.html b/lab/28-transactions/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/28-transactions/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/28-transactions/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..e5ae22b
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,38 @@
+package rewards.internal.reward;
+
+import javax.sql.DataSource;
+
+import org.springframework.jdbc.core.JdbcTemplate;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+import common.datetime.SimpleDate;
+
+/**
+ * JDBC implementation of a reward repository that records the result of a reward transaction by inserting a reward
+ * confirmation record.
+ */
+public class JdbcRewardRepository implements RewardRepository {
+
+ private JdbcTemplate jdbcTemplate;
+
+ public void setDataSource(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ String confirmationNumber = nextConfirmationNumber();
+ jdbcTemplate.update(sql, confirmationNumber, contribution.getAmount().asBigDecimal(),
+ SimpleDate.today().asDate(), contribution.getAccountNumber(), dining.getMerchantNumber(),
+ dining.getDate().asDate(), dining.getAmount().asBigDecimal());
+ return new RewardConfirmation(confirmationNumber, contribution);
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ return jdbcTemplate.queryForObject(sql, String.class);
+ }
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/28-transactions/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..1207f0f
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/28-transactions/src/main/java/rewards/internal/reward/package.html b/lab/28-transactions/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/28-transactions/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/account/Beneficiary.java b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..33392fd
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,90 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.ResultSetExtractor;
+
+import javax.sql.DataSource;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+public class JdbcAccountRepository implements AccountRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public JdbcAccountRepository(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ /**
+ * Extracts an Account object from rows returned from a join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY.
+ */
+ private final ResultSetExtractor accountExtractor = new AccountExtractor();
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = "select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?";
+ return jdbcTemplate.query(sql, accountExtractor, creditCardNumber);
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ for (Beneficiary b : account.getBeneficiaries()) {
+ jdbcTemplate.update(sql, b.getSavings().asBigDecimal(), account.getEntityId(), b.getName());
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ *
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+
+ private class AccountExtractor implements ResultSetExtractor {
+
+ public Account extractData(ResultSet rs) throws SQLException, DataAccessException {
+ return mapAccount(rs);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/account/package.html b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
new file mode 100644
index 0000000..b7d6d74
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
@@ -0,0 +1,20 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * Determines if benefit is available for an account for dining.
+ *
+ * A value object. A strategy. Scoped by the Resturant aggregate.
+ */
+public interface BenefitAvailabilityPolicy {
+
+ /**
+ * Calculates if an account is eligible to receive benefits for a dining.
+ * @param account the account of the member who dined
+ * @param dining the dining event
+ * @return benefit availability status
+ */
+ boolean isBenefitAvailableFor(Account account, Dining dining);
+}
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..62f11a6
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,114 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import javax.sql.DataSource;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ */
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public JdbcRestaurantRepository(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ */
+ private final RowMapper rowMapper = new RestaurantRowMapper();
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ String sql = "select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY from T_RESTAURANT where MERCHANT_NUMBER = ?";
+ return jdbcTemplate.queryForObject(sql, rowMapper, merchantNumber);
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+ // map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ restaurant.setBenefitAvailabilityPolicy(mapBenefitAvailabilityPolicy(rs));
+ return restaurant;
+ }
+
+ /**
+ * Helper method that maps benefit availability policy data in the ResultSet to a fully-configured
+ * {@link BenefitAvailabilityPolicy} object. The key column is 'BENEFIT_AVAILABILITY_POLICY', which is a
+ * discriminator column containing a string code that identifies the type of policy. Currently supported types are:
+ * 'A' for 'always available' and 'N' for 'never available'.
+ *
+ *
+ * More types could be added easily by enhancing this method. For example, 'W' for 'Weekdays only' or 'M' for 'Max
+ * Rewards per Month'. Some of these types might require additional database column values to be configured, for
+ * example a 'MAX_REWARDS_PER_MONTH' data column.
+ *
+ * @param rs the result set used to map the policy object from database column values
+ * @return the matching benefit availability policy
+ * @throws IllegalArgumentException if the mapping could not be performed
+ */
+ private BenefitAvailabilityPolicy mapBenefitAvailabilityPolicy(ResultSet rs) throws SQLException {
+ String policyCode = rs.getString("BENEFIT_AVAILABILITY_POLICY");
+ if ("A".equals(policyCode)) {
+ return AlwaysAvailable.INSTANCE;
+ } else if ("N".equals(policyCode)) {
+ return NeverAvailable.INSTANCE;
+ } else {
+ throw new IllegalArgumentException("Not a supported policy code " + policyCode);
+ }
+ }
+
+ /**
+ * Returns true indicating benefit is always available.
+ */
+ static class AlwaysAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new AlwaysAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+
+ public String toString() {
+ return "alwaysAvailable";
+ }
+ }
+
+ /**
+ * Returns false indicating benefit is never available.
+ */
+ static class NeverAvailable implements BenefitAvailabilityPolicy {
+ static final BenefitAvailabilityPolicy INSTANCE = new NeverAvailable();
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return false;
+ }
+
+ public String toString() {
+ return "neverAvailable";
+ }
+ }
+
+ private class RestaurantRowMapper implements RowMapper {
+
+ public Restaurant mapRow(ResultSet rs, int rowNum) throws SQLException {
+ return mapRestaurant(rs);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/Restaurant.java b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
new file mode 100644
index 0000000..50399f6
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/Restaurant.java
@@ -0,0 +1,101 @@
+package rewards.internal.restaurant;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * A restaurant establishment in the network. Like AppleBee's.
+ *
+ * Restaurants calculate how much benefit may be awarded to an account for dining based on an availability policy and a
+ * benefit percentage.
+ */
+public class Restaurant extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Percentage benefitPercentage;
+
+ private BenefitAvailabilityPolicy benefitAvailabilityPolicy;
+
+ @SuppressWarnings("unused")
+ private Restaurant() {
+ }
+
+ /**
+ * Creates a new restaurant.
+ * @param number the restaurant's merchant number
+ * @param name the name of the restaurant
+ */
+ public Restaurant(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Sets the percentage benefit to be awarded for eligible dining transactions.
+ * @param benefitPercentage the benefit percentage
+ */
+ public void setBenefitPercentage(Percentage benefitPercentage) {
+ this.benefitPercentage = benefitPercentage;
+ }
+
+ /**
+ * Sets the policy that determines if a dining by an account at this restaurant is eligible for benefit.
+ * @param benefitAvailabilityPolicy the benefit availability policy
+ */
+ public void setBenefitAvailabilityPolicy(BenefitAvailabilityPolicy benefitAvailabilityPolicy) {
+ this.benefitAvailabilityPolicy = benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Returns the name of this restaurant.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the merchant number of this restaurant.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns this restaurant's benefit percentage.
+ */
+ public Percentage getBenefitPercentage() {
+ return benefitPercentage;
+ }
+
+ /**
+ * Returns this restaurant's benefit availability policy.
+ */
+ public BenefitAvailabilityPolicy getBenefitAvailabilityPolicy() {
+ return benefitAvailabilityPolicy;
+ }
+
+ /**
+ * Calculate the benefit eligible to this account for dining at this restaurant.
+ * @param account the account that dined at this restaurant
+ * @param dining a dining event that occurred
+ * @return the benefit amount eligible for reward
+ */
+ public MonetaryAmount calculateBenefitFor(Account account, Dining dining) {
+ if (benefitAvailabilityPolicy.isBenefitAvailableFor(account, dining)) {
+ return dining.getAmount().multiplyBy(benefitPercentage);
+ } else {
+ return MonetaryAmount.zero();
+ }
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = '" + name + "', benefitPercentage = " + benefitPercentage
+ + ", benefitAvailabilityPolicy = " + benefitAvailabilityPolicy;
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
new file mode 100644
index 0000000..6bad2ef
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/RestaurantRepository.java
@@ -0,0 +1,17 @@
+package rewards.internal.restaurant;
+
+/**
+ * Loads restaurant aggregates. Called by the reward network to find and reconstitute Restaurant entities from an
+ * external form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface RestaurantRepository {
+
+ /**
+ * Load a Restaurant entity by its merchant number.
+ * @param merchantNumber the merchant number
+ * @return the restaurant
+ */
+ Restaurant findByMerchantNumber(String merchantNumber);
+}
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/package.html b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/package.html
new file mode 100644
index 0000000..96aff8d
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/restaurant/package.html
@@ -0,0 +1,7 @@
+
+
+
+The Restaurant module.
+
+
+
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
new file mode 100644
index 0000000..04f6502
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/reward/JdbcRewardRepository.java
@@ -0,0 +1,36 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import org.springframework.jdbc.core.JdbcTemplate;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+import javax.sql.DataSource;
+
+/**
+ * JDBC implementation of a reward repository that records the result of a reward transaction by inserting a reward
+ * confirmation record.
+ */
+public class JdbcRewardRepository implements RewardRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public JdbcRewardRepository(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ String sql = "insert into T_REWARD (CONFIRMATION_NUMBER, REWARD_AMOUNT, REWARD_DATE, ACCOUNT_NUMBER, DINING_MERCHANT_NUMBER, DINING_DATE, DINING_AMOUNT) values (?, ?, ?, ?, ?, ?, ?)";
+ String confirmationNumber = nextConfirmationNumber();
+ jdbcTemplate.update(sql, confirmationNumber, contribution.getAmount().asBigDecimal(),
+ SimpleDate.today().asDate(), contribution.getAccountNumber(), dining.getMerchantNumber(),
+ dining.getDate().asDate(), dining.getAmount().asBigDecimal());
+ return new RewardConfirmation(confirmationNumber, contribution);
+ }
+
+ private String nextConfirmationNumber() {
+ String sql = "select next value for S_REWARD_CONFIRMATION_NUMBER from DUAL_REWARD_CONFIRMATION_NUMBER";
+ return jdbcTemplate.queryForObject(sql, String.class);
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/reward/RewardRepository.java b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/reward/RewardRepository.java
new file mode 100644
index 0000000..1207f0f
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/reward/RewardRepository.java
@@ -0,0 +1,20 @@
+package rewards.internal.reward;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+
+/**
+ * Handles creating records of reward transactions to track contributions made to accounts for dining at restaurants.
+ */
+public interface RewardRepository {
+
+ /**
+ * Create a record of a reward that will track a contribution made to an account for dining.
+ * @param contribution the account contribution that was made
+ * @param dining the dining event that resulted in the account contribution
+ * @return a reward confirmation object that can be used for reporting and to lookup the reward details at a later
+ * date
+ */
+ RewardConfirmation confirmReward(AccountContribution contribution, Dining dining);
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/reward/package.html b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/reward/package.html
new file mode 100644
index 0000000..80e1b31
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/java/rewards/internal/reward/package.html
@@ -0,0 +1,7 @@
+
+
+
+The public interface of the rewards application defined by the central RewardNetwork.
+
+
+
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/resources/application.properties b/lab/32-jdbc-autoconfig-solution/src/main/resources/application.properties
new file mode 100644
index 0000000..aa13221
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/resources/application.properties
@@ -0,0 +1,10 @@
+logging.level.root=ERROR
+logging.level.rewards=INFO
+logging.level.config=DEBUG
+
+rewards.recipient.name=John Doe
+rewards.recipient.age=10
+rewards.recipient.gender=Male
+rewards.recipient.hobby=Tennis
+
+spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/resources/data.sql b/lab/32-jdbc-autoconfig-solution/src/main/resources/data.sql
new file mode 100644
index 0000000..28a87cc
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/resources/data.sql
@@ -0,0 +1,78 @@
+
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456789', 'Keith and Keri Donald');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456001', 'Dollie R. Adams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456002', 'Cornelia J. Andresen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456003', 'Coral Villareal Betancourt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456004', 'Chad I. Cobbs');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456005', 'Michael C. Feller');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456006', 'Michael J. Grover');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456007', 'John C. Howard');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456008', 'Ida Ketterer');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456009', 'Laina Ochoa Lucero');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456010', 'Wesley M. Mayo');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456011', 'Leslie F. Mcclary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456012', 'John D. Mudra');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456013', 'Pietronella J. Nielsen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456014', 'John S. Oleary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456015', 'Glenda D. Smith');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456016', 'Willemina O. Thygesen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456017', 'Antje Vogt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456018', 'Julia Weber');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456019', 'Mark T. Williams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456020', 'Christine J. Wilson');
+
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (0, '1234123412341234');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (1, '1234123412340001');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (2, '1234123412340002');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (3, '1234123412340003');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (4, '1234123412340004');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (5, '1234123412340005');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (6, '1234123412340006');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (7, '1234123412340007');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (8, '1234123412340008');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (9, '1234123412340009');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (10, '1234123412340010');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (11, '1234123412340011');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (12, '1234123412340012');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (13, '1234123412340013');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (14, '1234123412340014');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (15, '1234123412340015');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (16, '1234123412340016');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (17, '1234123412340017');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (18, '1234123412340018');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (19, '1234123412340019');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (20, '1234123412340020');
+
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (0, 'Annabelle', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (0, 'Corgan', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Antolin', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Argus', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Gian', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Argeo', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Kai', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Kasper', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Ernst', .34, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (12, 'Brian', .75, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (12, 'Shelby', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Charles', .50, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Thomas', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Neil', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (17, 'Daniel', 1.0, 0.00);
+
+insert into T_RESTAURANT (MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY)
+ values ('1234567890', 'AppleBees', .08, 'A');
diff --git a/lab/32-jdbc-autoconfig-solution/src/main/resources/schema.sql b/lab/32-jdbc-autoconfig-solution/src/main/resources/schema.sql
new file mode 100644
index 0000000..b0324fa
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/main/resources/schema.sql
@@ -0,0 +1,20 @@
+drop table T_ACCOUNT_BENEFICIARY if exists;
+drop table T_ACCOUNT_CREDIT_CARD if exists;
+drop table T_ACCOUNT if exists;
+drop table T_RESTAURANT if exists;
+drop table T_REWARD if exists;
+drop sequence S_REWARD_CONFIRMATION_NUMBER if exists;
+drop table DUAL_REWARD_CONFIRMATION_NUMBER if exists;
+
+create table T_ACCOUNT (ID integer identity primary key, NUMBER varchar(9), NAME varchar(50) not null, unique(NUMBER));
+create table T_ACCOUNT_CREDIT_CARD (ID integer identity primary key, ACCOUNT_ID integer, NUMBER varchar(16), unique(ACCOUNT_ID, NUMBER));
+create table T_ACCOUNT_BENEFICIARY (ID integer identity primary key, ACCOUNT_ID integer, NAME varchar(50), ALLOCATION_PERCENTAGE decimal(3,2) not null, SAVINGS decimal(8,2) not null, unique(ACCOUNT_ID, NAME));
+create table T_RESTAURANT (ID integer identity primary key, MERCHANT_NUMBER varchar(10) not null, NAME varchar(80) not null, BENEFIT_PERCENTAGE decimal(3,2) not null, BENEFIT_AVAILABILITY_POLICY varchar(1) not null, unique(MERCHANT_NUMBER));
+create table T_REWARD (ID integer identity primary key, CONFIRMATION_NUMBER varchar(25) not null, REWARD_AMOUNT decimal(8,2) not null, REWARD_DATE date not null, ACCOUNT_NUMBER varchar(9) not null, DINING_AMOUNT decimal not null, DINING_MERCHANT_NUMBER varchar(10) not null, DINING_DATE date not null, unique(CONFIRMATION_NUMBER));
+
+create sequence S_REWARD_CONFIRMATION_NUMBER start with 1;
+create table DUAL_REWARD_CONFIRMATION_NUMBER (ZERO integer);
+insert into DUAL_REWARD_CONFIRMATION_NUMBER values (0);
+
+alter table T_ACCOUNT_CREDIT_CARD add constraint FK_ACCOUNT_CREDIT_CARD foreign key (ACCOUNT_ID) references T_ACCOUNT(ID) on delete cascade;
+alter table T_ACCOUNT_BENEFICIARY add constraint FK_ACCOUNT_BENEFICIARY foreign key (ACCOUNT_ID) references T_ACCOUNT(ID) on delete cascade;
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/RewardNetworkTests.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/RewardNetworkTests.java
new file mode 100644
index 0000000..ed7fa9d
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/RewardNetworkTests.java
@@ -0,0 +1,53 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * A system test that verifies the components of the RewardNetwork application work together to reward for dining
+ * successfully. Uses Spring to bootstrap the application for use in a test environment.
+ */
+@SpringBootTest
+public class RewardNetworkTests {
+
+ /**
+ * The object being tested.
+ */
+ @Autowired
+ private RewardNetwork rewardNetwork;
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the contribution account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/SystemTestConfig.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/SystemTestConfig.java
new file mode 100644
index 0000000..1b15e84
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/SystemTestConfig.java
@@ -0,0 +1,32 @@
+package rewards;
+
+import config.RewardsConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+
+/**
+ * Sets up an embedded in-memory HSQL database, primarily for testing.
+ */
+@Configuration
+@Import(RewardsConfig.class)
+public class SystemTestConfig {
+ private final Logger logger = LoggerFactory.getLogger(SystemTestConfig.class);
+
+ /**
+ * Creates an in-memory "rewards" database populated
+ * with test data for fast testing
+ */
+// @Bean
+// public DataSource dataSource() {
+// logger.debug("Creating the datasource bean explicitly");
+//
+// return
+// (new EmbeddedDatabaseBuilder())
+// .addScript("classpath:rewards/testdb/schema.sql")
+// .addScript("classpath:rewards/testdb/data.sql")
+// .build();
+// }
+
+}
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
new file mode 100644
index 0000000..77a6039
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/RewardNetworkImplTests.java
@@ -0,0 +1,70 @@
+package rewards.internal;
+
+import common.money.MonetaryAmount;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Unit tests for the RewardNetworkImpl application logic. Configures the implementation with stub repositories
+ * containing dummy data for fast in-memory testing without the overhead of an external data source.
+ *
+ * Besides helping catch bugs early, tests are a great way for a new developer to learn an API as he or she can see the
+ * API in action. Tests also help validate a design as they are a measure for how easy it is to use your code.
+ */
+public class RewardNetworkImplTests {
+
+ /**
+ * The object being tested.
+ */
+ private RewardNetworkImpl rewardNetwork;
+
+ @BeforeEach
+ public void setUp() {
+ // create stubs to facilitate fast in-memory testing with dummy data and no external dependencies
+ AccountRepository accountRepo = new StubAccountRepository();
+ RestaurantRepository restaurantRepo = new StubRestaurantRepository();
+ RewardRepository rewardRepo = new StubRewardRepository();
+
+ // setup the object being tested by handing what it needs to work
+ rewardNetwork = new RewardNetworkImpl(accountRepo, restaurantRepo, rewardRepo);
+ }
+
+ @Test
+ public void testRewardForDining() {
+ // create a new dining of 100.00 charged to credit card '1234123412341234' by merchant '123457890' as test input
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+
+ // call the 'rewardNetwork' to test its rewardAccountFor(Dining) method
+ RewardConfirmation confirmation = rewardNetwork.rewardAccountFor(dining);
+
+ // assert the expected reward confirmation results
+ assertNotNull(confirmation);
+ assertNotNull(confirmation.getConfirmationNumber());
+
+ // assert an account contribution was made
+ AccountContribution contribution = confirmation.getAccountContribution();
+ assertNotNull(contribution);
+
+ // the account number should be '123456789'
+ assertEquals("123456789", contribution.getAccountNumber());
+
+ // the total contribution amount should be 8.00 (8% of 100.00)
+ assertEquals(MonetaryAmount.valueOf("8.00"), contribution.getAmount());
+
+ // the total contribution amount should have been split into 2 distributions
+ assertEquals(2, contribution.getDistributions().size());
+
+ // each distribution should be 4.00 (as both have a 50% allocation)
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("4.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/StubAccountRepository.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/StubAccountRepository.java
new file mode 100644
index 0000000..41c4df5
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/StubAccountRepository.java
@@ -0,0 +1,41 @@
+package rewards.internal;
+
+import common.money.Percentage;
+import org.springframework.dao.EmptyResultDataAccessException;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A dummy account repository implementation. Has a single Account "Keith and Keri Donald" with two beneficiaries
+ * "Annabelle" (50% allocation) and "Corgan" (50% allocation) associated with credit card "1234123412341234".
+ *
+ * Stubs facilitate unit testing. An object needing an AccountRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubAccountRepository implements AccountRepository {
+
+ private final Map accountsByCreditCard = new HashMap<>();
+
+ public StubAccountRepository() {
+ Account account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ accountsByCreditCard.put("1234123412341234", account);
+ }
+
+ public Account findByCreditCard(String creditCardNumber) {
+ Account account = accountsByCreditCard.get(creditCardNumber);
+ if (account == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ public void updateBeneficiaries(Account account) {
+ // nothing to do, everything is in memory
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/StubRestaurantRepository.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
new file mode 100644
index 0000000..13993a9
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/StubRestaurantRepository.java
@@ -0,0 +1,51 @@
+package rewards.internal;
+
+import common.money.Percentage;
+import org.springframework.dao.EmptyResultDataAccessException;
+import rewards.Dining;
+import rewards.internal.account.Account;
+import rewards.internal.restaurant.BenefitAvailabilityPolicy;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A dummy restaurant repository implementation. Has a single restaurant "Apple Bees" with a 8% benefit availability
+ * percentage that's always available.
+ *
+ * Stubs facilitate unit testing. An object needing a RestaurantRepository can work with this stub and not have to bring
+ * in expensive and/or complex dependencies such as a Database. Simple unit tests can then verify object behavior by
+ * considering the state of this stub.
+ */
+public class StubRestaurantRepository implements RestaurantRepository {
+
+ private final Map restaurantsByMerchantNumber = new HashMap<>();
+
+ public StubRestaurantRepository() {
+ Restaurant restaurant = new Restaurant("1234567890", "Apple Bees");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new AlwaysReturnsTrue());
+ restaurantsByMerchantNumber.put(restaurant.getNumber(), restaurant);
+ }
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ Restaurant restaurant = (Restaurant) restaurantsByMerchantNumber.get(merchantNumber);
+ if (restaurant == null) {
+ throw new EmptyResultDataAccessException(1);
+ }
+ return restaurant;
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy that always returns true. Only useful for testing--a real
+ * availability policy might consider many factors such as the day of week of the dining, or the account's reward
+ * history for the current month.
+ */
+ private static class AlwaysReturnsTrue implements BenefitAvailabilityPolicy {
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/StubRewardRepository.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/StubRewardRepository.java
new file mode 100644
index 0000000..d149acc
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/StubRewardRepository.java
@@ -0,0 +1,22 @@
+package rewards.internal;
+
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.reward.RewardRepository;
+
+import java.util.Random;
+
+/**
+ * A dummy reward repository implementation.
+ */
+public class StubRewardRepository implements RewardRepository {
+
+ public RewardConfirmation confirmReward(AccountContribution contribution, Dining dining) {
+ return new RewardConfirmation(confirmationNumber(), contribution);
+ }
+
+ private String confirmationNumber() {
+ return new Random().toString();
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/account/AccountTests.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/account/AccountTests.java
new file mode 100644
index 0000000..d2a53ae
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/account/AccountTests.java
@@ -0,0 +1,53 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.Test;
+import rewards.AccountContribution;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Unit tests for the Account class that verify Account behavior works in isolation.
+ */
+public class AccountTests {
+
+ private final Account account = new Account("1", "Keith and Keri Donald");
+
+ @Test
+ public void accountIsValid() {
+ // setup account with a valid set of beneficiaries to prepare for testing
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ assertTrue(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWithNoBeneficiaries() {
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreOver100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("100%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void accountIsInvalidWhenBeneficiaryAllocationsAreUnder100() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("25%"));
+ assertFalse(account.isValid());
+ }
+
+ @Test
+ public void makeContribution() {
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("100.00"));
+ assertEquals(contribution.getAmount(), MonetaryAmount.valueOf("100.00"));
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Annabelle").getAmount());
+ assertEquals(MonetaryAmount.valueOf("50.00"), contribution.getDistribution("Corgan").getAmount());
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
new file mode 100644
index 0000000..b1615ad
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/account/JdbcAccountRepositoryTests.java
@@ -0,0 +1,95 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC account repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcAccountRepositoryTests {
+
+ private JdbcAccountRepository repository;
+
+ private DataSource dataSource;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ repository = new JdbcAccountRepository(dataSource);
+ }
+
+ @Test
+ public void testFindAccountByCreditCard() {
+ Account account = repository.findByCreditCard("1234123412341234");
+ // assert the returned account contains what you expect given the state of the database
+ assertNotNull(account, "account should never be null");
+ assertEquals(Long.valueOf(0), account.getEntityId(), "wrong entity id");
+ assertEquals("123456789", account.getNumber(), "wrong account number");
+ assertEquals("Keith and Keri Donald", account.getName(), "wrong name");
+ assertEquals(2, account.getBeneficiaries().size(), "wrong beneficiary collection size");
+
+ Beneficiary b1 = account.getBeneficiary("Annabelle");
+ assertNotNull(b1, "Annabelle should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b1.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b1.getAllocationPercentage(), "wrong allocation percentage");
+
+ Beneficiary b2 = account.getBeneficiary("Corgan");
+ assertNotNull(b2, "Corgan should be a beneficiary");
+ assertEquals(MonetaryAmount.valueOf("0.00"), b2.getSavings(), "wrong savings");
+ assertEquals(Percentage.valueOf("50%"), b2.getAllocationPercentage(), "wrong allocation percentage");
+ }
+
+ @Test
+ public void testFindAccountByCreditCardNoAccount() {
+ assertThrows(EmptyResultDataAccessException.class, () -> {
+ repository.findByCreditCard("bogus");
+ });
+ }
+
+ @Test
+ public void testUpdateBeneficiaries() throws SQLException {
+ Account account = repository.findByCreditCard("1234123412341234");
+ account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ repository.updateBeneficiaries(account);
+ verifyBeneficiaryTableUpdated();
+ }
+
+ private void verifyBeneficiaryTableUpdated() throws SQLException {
+ String sql = "select SAVINGS from T_ACCOUNT_BENEFICIARY where NAME = ? and ACCOUNT_ID = ?";
+ PreparedStatement stmt = dataSource.getConnection().prepareStatement(sql);
+
+ // assert Annabelle has $4.00 savings now
+ stmt.setString(1, "Annabelle");
+ stmt.setLong(2, 0L);
+ ResultSet rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+
+ // assert Corgan has $4.00 savings now
+ stmt.setString(1, "Corgan");
+ stmt.setLong(2, 0L);
+ rs = stmt.executeQuery();
+ rs.next();
+ assertEquals(MonetaryAmount.valueOf("4.00"), MonetaryAmount.valueOf(rs.getString(1)));
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
new file mode 100644
index 0000000..06ca758
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/restaurant/JdbcRestaurantRepositoryTests.java
@@ -0,0 +1,51 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+
+import javax.sql.DataSource;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Tests the JDBC restaurant repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRestaurantRepositoryTests {
+
+ private JdbcRestaurantRepository repository;
+
+ @BeforeEach
+ public void setUp() {
+ repository = new JdbcRestaurantRepository(createTestDataSource());
+ }
+
+ @Test
+ public void testFindRestaurantByMerchantNumber() {
+ Restaurant restaurant = repository.findByMerchantNumber("1234567890");
+ assertNotNull(restaurant, "the restaurant should never be null");
+ assertEquals("1234567890", restaurant.getNumber(), "the merchant number is wrong");
+ assertEquals("AppleBees", restaurant.getName(), "the name is wrong");
+ assertEquals(Percentage.valueOf("8%"), restaurant.getBenefitPercentage(), "the benefitPercentage is wrong");
+ assertEquals(JdbcRestaurantRepository.AlwaysAvailable.INSTANCE,
+ restaurant.getBenefitAvailabilityPolicy(), "the benefit availability policy is wrong");
+ }
+
+ @Test
+ public void testFindRestaurantByBogusMerchantNumber() {
+ assertThrows(EmptyResultDataAccessException.class, ()-> {
+ repository.findByMerchantNumber("bogus");
+ });
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java
new file mode 100644
index 0000000..450e81c
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/restaurant/RestaurantTests.java
@@ -0,0 +1,69 @@
+package rewards.internal.restaurant;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * Unit tests for exercising the behavior of the Restaurant aggregate entity. A restaurant calculates a benefit to award
+ * to an account for dining based on an availability policy and benefit percentage.
+ */
+public class RestaurantTests {
+
+ private Restaurant restaurant;
+
+ private Account account;
+
+ private Dining dining;
+
+ @BeforeEach
+ public void setUp() {
+ // configure the restaurant, the object being tested
+ restaurant = new Restaurant("1234567890", "AppleBee's");
+ restaurant.setBenefitPercentage(Percentage.valueOf("8%"));
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(true));
+ // configure supporting objects needed by the restaurant
+ account = new Account("123456789", "Keith and Keri Donald");
+ account.addBeneficiary("Annabelle");
+ dining = Dining.createDining("100.00", "1234123412341234", "1234567890");
+ }
+
+ @Test
+ public void testCalcuateBenefitFor() {
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert 8.00 eligible for reward
+ assertEquals(MonetaryAmount.valueOf("8.00"), benefit);
+ }
+
+ @Test
+ public void testNoBenefitAvailable() {
+ // configure stub that always returns false
+ restaurant.setBenefitAvailabilityPolicy(new StubBenefitAvailibilityPolicy(false));
+ MonetaryAmount benefit = restaurant.calculateBenefitFor(account, dining);
+ // assert zero eligible for reward
+ assertEquals(MonetaryAmount.valueOf("0.00"), benefit);
+ }
+
+ /**
+ * A simple "dummy" benefit availability policy containing a single flag used to determine if benefit is available.
+ * Only useful for testing--a real availability policy might consider many factors such as the day of week of the
+ * dining, or the account's reward history for the current month.
+ */
+ private static class StubBenefitAvailibilityPolicy implements BenefitAvailabilityPolicy {
+
+ private final boolean isBenefitAvailable;
+
+ public StubBenefitAvailibilityPolicy(boolean isBenefitAvailable) {
+ this.isBenefitAvailable = isBenefitAvailable;
+ }
+
+ public boolean isBenefitAvailableFor(Account account, Dining dining) {
+ return isBenefitAvailable;
+ }
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
new file mode 100644
index 0000000..be8f7cc
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/java/rewards/internal/reward/JdbcRewardRepositoryTests.java
@@ -0,0 +1,88 @@
+package rewards.internal.reward;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.internal.account.Account;
+
+import javax.sql.DataSource;
+import java.math.BigDecimal;
+import java.sql.SQLException;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * Tests the JDBC reward repository with a test data source to verify data access and relational-to-object mapping
+ * behavior works as expected.
+ */
+public class JdbcRewardRepositoryTests {
+
+ private JdbcRewardRepository repository;
+
+ private DataSource dataSource;
+
+ private JdbcTemplate jdbcTemplate;
+
+ @BeforeEach
+ public void setUp() {
+ dataSource = createTestDataSource();
+ repository = new JdbcRewardRepository(dataSource);
+ jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ @Test
+ public void testCreateReward() throws SQLException {
+ Dining dining = Dining.createDining("100.00", "1234123412341234", "0123456789");
+
+ Account account = new Account("1", "Keith and Keri Donald");
+ account.setEntityId(0L);
+ account.addBeneficiary("Annabelle", Percentage.valueOf("50%"));
+ account.addBeneficiary("Corgan", Percentage.valueOf("50%"));
+
+ AccountContribution contribution = account.makeContribution(MonetaryAmount.valueOf("8.00"));
+ RewardConfirmation confirmation = repository.confirmReward(contribution, dining);
+ assertNotNull(confirmation, "confirmation should not be null");
+ assertNotNull(confirmation.getConfirmationNumber(), "confirmation number should not be null");
+ assertEquals(contribution, confirmation.getAccountContribution(), "wrong contribution object");
+ verifyRewardInserted(confirmation, dining);
+ }
+
+ private void verifyRewardInserted(RewardConfirmation confirmation, Dining dining) {
+ assertEquals(1, getRewardCount());
+ String sql = "select * from T_REWARD where CONFIRMATION_NUMBER = ?";
+ Map values = jdbcTemplate.queryForMap(sql, confirmation.getConfirmationNumber());
+ verifyInsertedValues(confirmation, dining, values);
+ }
+
+ private void verifyInsertedValues(RewardConfirmation confirmation, Dining dining, Map values) {
+ assertEquals(confirmation.getAccountContribution().getAmount(), new MonetaryAmount((BigDecimal) values
+ .get("REWARD_AMOUNT")));
+ assertEquals(SimpleDate.today().asDate(), values.get("REWARD_DATE"));
+ assertEquals(confirmation.getAccountContribution().getAccountNumber(), values.get("ACCOUNT_NUMBER"));
+ assertEquals(dining.getAmount(), new MonetaryAmount((BigDecimal) values.get("DINING_AMOUNT")));
+ assertEquals(dining.getMerchantNumber(), values.get("DINING_MERCHANT_NUMBER"));
+ assertEquals(SimpleDate.today().asDate(), values.get("DINING_DATE"));
+ }
+
+ private int getRewardCount() {
+ String sql = "select count(*) from T_REWARD";
+ return jdbcTemplate.queryForObject(sql, Integer.class);
+ }
+
+ private DataSource createTestDataSource() {
+ return new EmbeddedDatabaseBuilder()
+ .setName("rewards")
+ .addScript("/rewards/testdb/schema.sql")
+ .addScript("/rewards/testdb/data.sql")
+ .build();
+ }
+}
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/resources/rewards/testdb/data.sql b/lab/32-jdbc-autoconfig-solution/src/test/resources/rewards/testdb/data.sql
new file mode 100644
index 0000000..28a87cc
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/resources/rewards/testdb/data.sql
@@ -0,0 +1,78 @@
+
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456789', 'Keith and Keri Donald');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456001', 'Dollie R. Adams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456002', 'Cornelia J. Andresen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456003', 'Coral Villareal Betancourt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456004', 'Chad I. Cobbs');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456005', 'Michael C. Feller');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456006', 'Michael J. Grover');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456007', 'John C. Howard');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456008', 'Ida Ketterer');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456009', 'Laina Ochoa Lucero');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456010', 'Wesley M. Mayo');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456011', 'Leslie F. Mcclary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456012', 'John D. Mudra');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456013', 'Pietronella J. Nielsen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456014', 'John S. Oleary');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456015', 'Glenda D. Smith');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456016', 'Willemina O. Thygesen');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456017', 'Antje Vogt');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456018', 'Julia Weber');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456019', 'Mark T. Williams');
+insert into T_ACCOUNT (NUMBER, NAME) values ('123456020', 'Christine J. Wilson');
+
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (0, '1234123412341234');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (1, '1234123412340001');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (2, '1234123412340002');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (3, '1234123412340003');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (4, '1234123412340004');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (5, '1234123412340005');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (6, '1234123412340006');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (7, '1234123412340007');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (8, '1234123412340008');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (9, '1234123412340009');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (10, '1234123412340010');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (11, '1234123412340011');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (12, '1234123412340012');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (13, '1234123412340013');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (14, '1234123412340014');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (15, '1234123412340015');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (16, '1234123412340016');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (17, '1234123412340017');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (18, '1234123412340018');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (19, '1234123412340019');
+insert into T_ACCOUNT_CREDIT_CARD (ACCOUNT_ID, NUMBER) values (20, '1234123412340020');
+
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (0, 'Annabelle', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (0, 'Corgan', .5, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Antolin', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Argus', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Gian', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (3, 'Argeo', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Kai', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Kasper', .33, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (8, 'Ernst', .34, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (12, 'Brian', .75, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (12, 'Shelby', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Charles', .50, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Thomas', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (15, 'Neil', .25, 0.00);
+insert into T_ACCOUNT_BENEFICIARY (ACCOUNT_ID, NAME, ALLOCATION_PERCENTAGE, SAVINGS)
+ values (17, 'Daniel', 1.0, 0.00);
+
+insert into T_RESTAURANT (MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY)
+ values ('1234567890', 'AppleBees', .08, 'A');
diff --git a/lab/32-jdbc-autoconfig-solution/src/test/resources/rewards/testdb/schema.sql b/lab/32-jdbc-autoconfig-solution/src/test/resources/rewards/testdb/schema.sql
new file mode 100644
index 0000000..b0324fa
--- /dev/null
+++ b/lab/32-jdbc-autoconfig-solution/src/test/resources/rewards/testdb/schema.sql
@@ -0,0 +1,20 @@
+drop table T_ACCOUNT_BENEFICIARY if exists;
+drop table T_ACCOUNT_CREDIT_CARD if exists;
+drop table T_ACCOUNT if exists;
+drop table T_RESTAURANT if exists;
+drop table T_REWARD if exists;
+drop sequence S_REWARD_CONFIRMATION_NUMBER if exists;
+drop table DUAL_REWARD_CONFIRMATION_NUMBER if exists;
+
+create table T_ACCOUNT (ID integer identity primary key, NUMBER varchar(9), NAME varchar(50) not null, unique(NUMBER));
+create table T_ACCOUNT_CREDIT_CARD (ID integer identity primary key, ACCOUNT_ID integer, NUMBER varchar(16), unique(ACCOUNT_ID, NUMBER));
+create table T_ACCOUNT_BENEFICIARY (ID integer identity primary key, ACCOUNT_ID integer, NAME varchar(50), ALLOCATION_PERCENTAGE decimal(3,2) not null, SAVINGS decimal(8,2) not null, unique(ACCOUNT_ID, NAME));
+create table T_RESTAURANT (ID integer identity primary key, MERCHANT_NUMBER varchar(10) not null, NAME varchar(80) not null, BENEFIT_PERCENTAGE decimal(3,2) not null, BENEFIT_AVAILABILITY_POLICY varchar(1) not null, unique(MERCHANT_NUMBER));
+create table T_REWARD (ID integer identity primary key, CONFIRMATION_NUMBER varchar(25) not null, REWARD_AMOUNT decimal(8,2) not null, REWARD_DATE date not null, ACCOUNT_NUMBER varchar(9) not null, DINING_AMOUNT decimal not null, DINING_MERCHANT_NUMBER varchar(10) not null, DINING_DATE date not null, unique(CONFIRMATION_NUMBER));
+
+create sequence S_REWARD_CONFIRMATION_NUMBER start with 1;
+create table DUAL_REWARD_CONFIRMATION_NUMBER (ZERO integer);
+insert into DUAL_REWARD_CONFIRMATION_NUMBER values (0);
+
+alter table T_ACCOUNT_CREDIT_CARD add constraint FK_ACCOUNT_CREDIT_CARD foreign key (ACCOUNT_ID) references T_ACCOUNT(ID) on delete cascade;
+alter table T_ACCOUNT_BENEFICIARY add constraint FK_ACCOUNT_BENEFICIARY foreign key (ACCOUNT_ID) references T_ACCOUNT(ID) on delete cascade;
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/build.gradle b/lab/32-jdbc-autoconfig/build.gradle
new file mode 100644
index 0000000..045a70b
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/build.gradle
@@ -0,0 +1,17 @@
+// TO-DO-01 : Add the Spring Boot Plugin
+
+
+dependencies {
+ implementation project(':00-rewards-common')
+
+ // TO-DO-02 : Refactor from discrete Spring Framework
+ // dependency to JDBC Boot Starter
+ implementation 'org.springframework:spring-jdbc'
+
+}
+
+// Create original jar file with "gradlew assemble"
+jar {
+ enabled = true
+ archiveClassifier = 'original'
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/pom.xml b/lab/32-jdbc-autoconfig/pom.xml
new file mode 100644
index 0000000..ed3f260
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/pom.xml
@@ -0,0 +1,68 @@
+
+
+ 4.0.0
+ 32-jdbc-autoconfig
+
+ Spring Training
+ https://spring.io/training
+
+ jar
+
+ io.spring.training.core-spring
+ parentProject
+ 3.3.1
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+
+
+
+ org.springframework
+ spring-jdbc
+
+
+
+
+ org.springframework
+ spring-test
+ test
+
+
+
+
+ org.hsqldb
+ hsqldb
+
+
+
+
+ io.spring.training.core-spring
+ 00-rewards-common
+
+
+
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+
+
+
+
diff --git a/lab/32-jdbc-autoconfig/src/main/java/config/RewardsConfig.java b/lab/32-jdbc-autoconfig/src/main/java/config/RewardsConfig.java
new file mode 100644
index 0000000..11792d6
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/config/RewardsConfig.java
@@ -0,0 +1,77 @@
+package config;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import rewards.RewardNetwork;
+import rewards.internal.RewardNetworkImpl;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.account.JdbcAccountRepository;
+import rewards.internal.restaurant.JdbcRestaurantRepository;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.JdbcRewardRepository;
+import rewards.internal.reward.RewardRepository;
+
+import javax.sql.DataSource;
+
+@Configuration
+public class RewardsConfig {
+
+ private final Logger logger = LoggerFactory.getLogger(getClass());
+
+ final DataSource dataSource;
+
+ public RewardsConfig(DataSource dataSource) {
+ this.dataSource = dataSource;
+ }
+
+ // TODO-10 (Optional) : Switch back to explicit `DataSource` configuration
+ // (Instead of using auto-configured DataSource, we are going to configure
+ // our own DataSource bean. Normally we want to configure infra-structure
+ // bean such as DataSource bean in a separate configuration class but
+ // here for the sake of simplicity, we configure it along with application
+ // beans.)
+ // - Uncomment @Bean method below
+ // - Remove the code above that performs DataSource injection
+ // - Fix compile errors in this code
+ /*
+ @Bean
+ public DataSource dataSource() {
+ logger.debug("Creating the datasource bean explicitly");
+
+ return
+ (new EmbeddedDatabaseBuilder())
+ .addScript("classpath:schema.sql")
+ .addScript("classpath:data.sql")
+ .build();
+ }
+ */
+
+ @Bean
+ public RewardNetwork rewardNetwork() {
+ return new RewardNetworkImpl(
+ accountRepository(),
+ restaurantRepository(),
+ rewardRepository());
+ }
+
+ @Bean
+ public AccountRepository accountRepository() {
+ JdbcAccountRepository repository = new JdbcAccountRepository(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RestaurantRepository restaurantRepository() {
+ JdbcRestaurantRepository repository = new JdbcRestaurantRepository(dataSource);
+ return repository;
+ }
+
+ @Bean
+ public RewardRepository rewardRepository() {
+ JdbcRewardRepository repository = new JdbcRewardRepository(dataSource);
+ return repository;
+ }
+
+}
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/AccountContribution.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/AccountContribution.java
new file mode 100644
index 0000000..beb796d
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/AccountContribution.java
@@ -0,0 +1,138 @@
+package rewards;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+
+import java.util.Set;
+
+/**
+ * A summary of a monetary contribution made to an account that was distributed among the account's beneficiaries.
+ *
+ * A value object. Immutable.
+ */
+public class AccountContribution {
+
+ private String accountNumber;
+
+ private MonetaryAmount amount;
+
+ private Set distributions;
+
+ /**
+ * Creates a new account contribution.
+ * @param accountNumber the number of the account the contribution was made
+ * @param amount the total contribution amount
+ * @param distributions how the contribution was distributed among the account's beneficiaries
+ */
+ public AccountContribution(String accountNumber, MonetaryAmount amount, Set distributions) {
+ this.accountNumber = accountNumber;
+ this.amount = amount;
+ this.distributions = distributions;
+ }
+
+ /**
+ * Returns the number of the account this contribution was made to.
+ * @return the account number
+ */
+ public String getAccountNumber() {
+ return accountNumber;
+ }
+
+ /**
+ * Returns the total amount of the contribution.
+ * @return the contribution amount
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns how this contribution was distributed among the account's beneficiaries.
+ * @return the contribution distributions
+ */
+ public Set getDistributions() {
+ return distributions;
+ }
+
+ /**
+ * Returns how this contribution was distributed to a single account beneficiary.
+ * @param beneficiary the name of the beneficiary e.g "Annabelle"
+ * @return a summary of how the contribution amount was distributed to the beneficiary
+ */
+ public Distribution getDistribution(String beneficiary) {
+ for (Distribution d : distributions) {
+ if (d.beneficiary.equals(beneficiary)) {
+ return d;
+ }
+ }
+ throw new IllegalArgumentException("No such distribution for '" + beneficiary + "'");
+ }
+
+ /**
+ * A single distribution made to a beneficiary as part of an account contribution, summarizing the distribution
+ * amount and resulting total beneficiary savings.
+ *
+ * A value object.
+ */
+ public static class Distribution {
+
+ private String beneficiary;
+
+ private MonetaryAmount amount;
+
+ private Percentage percentage;
+
+ private MonetaryAmount totalSavings;
+
+ /**
+ * Creates a new distribution.
+ * @param beneficiary the name of the account beneficiary that received a distribution
+ * @param amount the distribution amount
+ * @param percentage this distribution's percentage of the total account contribution
+ * @param totalSavings the beneficiary's total savings amount after the distribution was made
+ */
+ public Distribution(String beneficiary, MonetaryAmount amount, Percentage percentage,
+ MonetaryAmount totalSavings) {
+ this.beneficiary = beneficiary;
+ this.percentage = percentage;
+ this.amount = amount;
+ this.totalSavings = totalSavings;
+ }
+
+ /**
+ * Returns the name of the beneficiary.
+ */
+ public String getBeneficiary() {
+ return beneficiary;
+ }
+
+ /**
+ * Returns the amount of this distribution.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the percentage of this distribution relative to others in the contribution.
+ */
+ public Percentage getPercentage() {
+ return percentage;
+ }
+
+ /**
+ * Returns the total savings of the beneficiary after this distribution.
+ */
+ public MonetaryAmount getTotalSavings() {
+ return totalSavings;
+ }
+
+ public String toString() {
+ return amount + " to '" + beneficiary + "' (" + percentage + ")";
+ }
+ }
+
+ public String toString() {
+ return "Contribution of " + amount + " to account '" + accountNumber + "' distributed " + distributions;
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/Dining.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/Dining.java
new file mode 100644
index 0000000..0df7466
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/Dining.java
@@ -0,0 +1,113 @@
+package rewards;
+
+import common.datetime.SimpleDate;
+import common.money.MonetaryAmount;
+
+/**
+ * A dining event that occurred, representing a charge made to a credit card by a merchant on a specific date.
+ *
+ * For a dining to be eligible for reward, the credit card number should map to an account in the reward network. In
+ * addition, the merchant number should map to a restaurant in the network.
+ *
+ * A value object. Immutable.
+ */
+public class Dining {
+
+ private MonetaryAmount amount;
+
+ private String creditCardNumber;
+
+ private String merchantNumber;
+
+ private SimpleDate date;
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a card by a merchant on the date specified.
+ * @param amount the total amount of the dining bill
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param date the date of the dining event
+ */
+ public Dining(MonetaryAmount amount, String creditCardNumber, String merchantNumber, SimpleDate date) {
+ this.amount = amount;
+ this.creditCardNumber = creditCardNumber;
+ this.merchantNumber = merchantNumber;
+ this.date = date;
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on today's date. A
+ * convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, SimpleDate.today());
+ }
+
+ /**
+ * Creates a new dining, reflecting an amount that was charged to a credit card by a merchant on the date specified.
+ * A convenient static factory method.
+ * @param amount the total amount of the dining bill as a string
+ * @param creditCardNumber the number of the credit card used to pay for the dining bill
+ * @param merchantNumber the merchant number of the restaurant where the dining occurred
+ * @param month the month of the dining event
+ * @param day the day of the dining event
+ * @param year the year of the dining event
+ * @return the dining event
+ */
+ public static Dining createDining(String amount, String creditCardNumber, String merchantNumber, int month,
+ int day, int year) {
+ return new Dining(MonetaryAmount.valueOf(amount), creditCardNumber, merchantNumber, new SimpleDate(month, day,
+ year));
+ }
+
+ /**
+ * Returns the amount of this dining--the total amount of the bill that was charged to the credit card.
+ */
+ public MonetaryAmount getAmount() {
+ return amount;
+ }
+
+ /**
+ * Returns the number of the credit card used to pay for this dining. For this dining to be eligible for reward,
+ * this credit card number should be associated with a valid account in the reward network.
+ */
+ public String getCreditCardNumber() {
+ return creditCardNumber;
+ }
+
+ /**
+ * Returns the merchant number of the restaurant where this dining occurred. For this dining to be eligible for
+ * reward, this merchant number should be associated with a valid restaurant in the reward network.
+ */
+ public String getMerchantNumber() {
+ return merchantNumber;
+ }
+
+ /**
+ * Returns the date this dining occurred on.
+ */
+ public SimpleDate getDate() {
+ return date;
+ }
+
+ public boolean equals(Object o) {
+ if (!(o instanceof Dining other)) {
+ return false;
+ }
+ // value objects are equal if their attributes are equal
+ return amount.equals(other.amount) && creditCardNumber.equals(other.creditCardNumber)
+ && merchantNumber.equals(other.merchantNumber) && date.equals(other.date);
+ }
+
+ public int hashCode() {
+ return amount.hashCode() + creditCardNumber.hashCode() + merchantNumber.hashCode() + date.hashCode();
+ }
+
+ public String toString() {
+ return "Dining of " + amount + " charged to '" + creditCardNumber + "' by '" + merchantNumber + "' on " + date;
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardConfirmation.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardConfirmation.java
new file mode 100644
index 0000000..c6984dc
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardConfirmation.java
@@ -0,0 +1,41 @@
+package rewards;
+
+/**
+ * A summary of a confirmed reward transaction describing a contribution made to an account that was distributed among
+ * the account's beneficiaries.
+ */
+public class RewardConfirmation {
+
+ private String confirmationNumber;
+
+ private AccountContribution accountContribution;
+
+ /**
+ * Creates a new reward confirmation.
+ * @param confirmationNumber the unique confirmation number
+ * @param accountContribution a summary of the account contribution that was made
+ */
+ public RewardConfirmation(String confirmationNumber, AccountContribution accountContribution) {
+ this.confirmationNumber = confirmationNumber;
+ this.accountContribution = accountContribution;
+ }
+
+ /**
+ * Returns the confirmation number of the reward transaction. Can be used later to lookup the transaction record.
+ */
+ public String getConfirmationNumber() {
+ return confirmationNumber;
+ }
+
+ /**
+ * Returns a summary of the monetary contribution that was made to an account.
+ * @return the account contribution (the details of this reward)
+ */
+ public AccountContribution getAccountContribution() {
+ return accountContribution;
+ }
+
+ public String toString() {
+ return confirmationNumber;
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardNetwork.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardNetwork.java
new file mode 100644
index 0000000..f17157b
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardNetwork.java
@@ -0,0 +1,28 @@
+package rewards;
+
+/**
+ * Rewards a member account for dining at a restaurant.
+ *
+ * A reward takes the form of a monetary contribution made to an account that is distributed among the account's
+ * beneficiaries. The contribution amount is typically a function of several factors such as the dining amount and
+ * restaurant where the dining occurred.
+ *
+ * Example: Papa Keith spends $100.00 at Apple Bee's resulting in a $8.00 contribution to his account that is
+ * distributed evenly among his beneficiaries Annabelle and Corgan.
+ *
+ * This is the central application-boundary for the "rewards" application. This is the public interface users call to
+ * invoke the application. This is the entry-point into the Application Layer.
+ */
+public interface RewardNetwork {
+
+ /**
+ * Reward an account for dining.
+ *
+ * For a dining to be eligible for reward: - It must have been paid for by a registered credit card of a valid
+ * member account in the network. - It must have taken place at a restaurant participating in the network.
+ *
+ * @param dining a charge made to a credit card for dining at a restaurant
+ * @return confirmation of the reward
+ */
+ RewardConfirmation rewardAccountFor(Dining dining);
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardsApplication.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardsApplication.java
new file mode 100644
index 0000000..6fdbde6
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardsApplication.java
@@ -0,0 +1,71 @@
+package rewards;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.SpringApplication;
+
+// TODO-00 : In this lab, you are going to exercise the following:
+// - Understanding how auto-configuration is triggered in Spring Boot application
+// - Using auto-configuring DataSource in test and application code
+// - Understanding how @SpringBootTest is used to create application context in test
+// - Implementing CommandLineRunner using auto-configured JdbcTemplate
+// - Disabling a particular auto-configuration
+// - Exercising the usage of @ConfigurationProperties
+
+// TODO-01 : Open pom.xml or build.gradle, look for TO-DO-01
+
+// TODO-02 : In pom.xml or build.gradle, look for TO-DO-02
+
+// TODO-03 : Turn this 'RewardsApplication' into a Spring Boot application
+// - Add an appropriate annotation to this class
+
+// --------------------------------------------
+
+// TODO-11 (Optional) : Disable 'DataSource' auto-configuration
+// - Note that you are using your own 'DataSource' bean now
+// instead of auto-configured one
+// - Use 'exclude' attribute of '@SpringBootApplication'
+// excluding 'DataSourceAutoConfiguration' class
+// - Run this application and observe a failure
+// - Import 'RewardsConfig' class
+// - Run this application again and observe a successful execution
+
+// TODO-12 (Optional) : Look in application.properties for the next step.
+
+// TODO-13 (Optional) : Follow the instruction in the lab document.
+// The section titled "Build and Run using Command Line tools".
+
+public class RewardsApplication {
+ static final String SQL = "SELECT count(*) FROM T_ACCOUNT";
+
+ final Logger logger
+ = LoggerFactory.getLogger(RewardsApplication.class);
+
+ public static void main(String[] args) {
+ SpringApplication.run(RewardsApplication.class, args);
+ }
+
+ // TODO-04 : Let Spring Boot execute database scripts
+ // - Move the SQL scripts (schema.sql and data.sql)
+ // from `src/test/resources/rewards/testdb` directory
+ // to `src/main/resources/` directory
+
+ // TODO-05 : Implement a command line runner that will query count from
+ // T_ACCOUNT table and log the count to the console
+ // - Use the SQL query and logger provided above.
+ // - Use the JdbcTemplate bean that Spring Boot auto-configured for you
+ // - Run this application and verify "Hello, there are 21 accounts" log message
+ // gets displayed in the console
+
+ // TODO-07 (Optional): Enable full debugging in order to observe how Spring Boot
+ // performs its auto-configuration logic
+ // - Follow TO-DO-07 in application.properties, then come back here.
+ // - Run the application
+ // - In the console output, find "CONDITIONS EVALUATION REPORT".
+ // It represents the auto-configuration logic used by Spring Boot.
+ // - Search for "JdbcTemplateAutoConfiguration matched:" and
+ // "DataSourceAutoConfiguration matched:". Note that each @Conditional*
+ // represents a single conditional statement in the "JdbcTemplateAutoConfiguration"
+ // and "DataSourceAutoConfiguration" classes.
+
+}
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardsRecipientProperties.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardsRecipientProperties.java
new file mode 100644
index 0000000..ad54529
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/RewardsRecipientProperties.java
@@ -0,0 +1,24 @@
+package rewards;
+
+// TODO-06 : Capture properties into a class using @ConfigurationProperties
+// - Note that application.properties file already contains the following properties
+//
+// rewards.recipient.name=John Doe
+// rewards.recipient.age=10
+// rewards.recipient.gender=Male
+// rewards.recipient.hobby=Tennis
+//
+// - Annotate this class with @ConfigurationProperties
+// with prefix attribute set to a proper value
+// - Create fields (along with needed getters/setters) that reflect the
+// properties above in the RewardsRecipientProperties class
+// - Use one of the following 3 schemes to enable @ConfigurationProperties
+// (1) Add @EnableConfigurationProperties(RewardsRecipientProperties.class)
+// to the RewardsApplication class
+// (2) Add @ConfigurationPropertiesScan to RewardsApplication class or
+// (3) Annotate this class with @Component
+// - Implement a new command line runner that displays the name of the rewards
+// recipient when the application gets started
+public class RewardsRecipientProperties {
+
+}
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/RewardNetworkImpl.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/RewardNetworkImpl.java
new file mode 100644
index 0000000..e8c8e07
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/RewardNetworkImpl.java
@@ -0,0 +1,51 @@
+package rewards.internal;
+
+import common.money.MonetaryAmount;
+import rewards.AccountContribution;
+import rewards.Dining;
+import rewards.RewardConfirmation;
+import rewards.RewardNetwork;
+import rewards.internal.account.Account;
+import rewards.internal.account.AccountRepository;
+import rewards.internal.restaurant.Restaurant;
+import rewards.internal.restaurant.RestaurantRepository;
+import rewards.internal.reward.RewardRepository;
+
+/**
+ * Rewards an Account for Dining at a Restaurant.
+ *
+ * The sole Reward Network implementation. This object is an application-layer service responsible for coordinating with
+ * the domain-layer to carry out the process of rewarding benefits to accounts for dining.
+ *
+ * Said in other words, this class implements the "reward account for dining" use case.
+ */
+public class RewardNetworkImpl implements RewardNetwork {
+
+ private final AccountRepository accountRepository;
+
+ private final RestaurantRepository restaurantRepository;
+
+ private final RewardRepository rewardRepository;
+
+ /**
+ * Creates a new reward network.
+ * @param accountRepository the repository for loading accounts to reward
+ * @param restaurantRepository the repository for loading restaurants that determine how much to reward
+ * @param rewardRepository the repository for recording a record of successful reward transactions
+ */
+ public RewardNetworkImpl(AccountRepository accountRepository, RestaurantRepository restaurantRepository,
+ RewardRepository rewardRepository) {
+ this.accountRepository = accountRepository;
+ this.restaurantRepository = restaurantRepository;
+ this.rewardRepository = rewardRepository;
+ }
+
+ public RewardConfirmation rewardAccountFor(Dining dining) {
+ Account account = accountRepository.findByCreditCard(dining.getCreditCardNumber());
+ Restaurant restaurant = restaurantRepository.findByMerchantNumber(dining.getMerchantNumber());
+ MonetaryAmount amount = restaurant.calculateBenefitFor(account, dining);
+ AccountContribution contribution = account.makeContribution(amount);
+ accountRepository.updateBeneficiaries(account);
+ return rewardRepository.confirmReward(contribution, dining);
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/Account.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/Account.java
new file mode 100644
index 0000000..c8f03de
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/Account.java
@@ -0,0 +1,158 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+import rewards.AccountContribution;
+import rewards.AccountContribution.Distribution;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * An account for a member of the reward network. An account has one or more beneficiaries whose allocations must add up
+ * to 100%.
+ *
+ * An account can make contributions to its beneficiaries. Each contribution is distributed among the beneficiaries
+ * based on an allocation.
+ *
+ * An entity. An aggregate.
+ */
+public class Account extends Entity {
+
+ private String number;
+
+ private String name;
+
+ private Set beneficiaries = new HashSet<>();
+
+ @SuppressWarnings("unused")
+ private Account() {
+ }
+
+ /**
+ * Create a new account.
+ * @param number the account number
+ * @param name the name on the account
+ */
+ public Account(String number, String name) {
+ this.number = number;
+ this.name = name;
+ }
+
+ /**
+ * Returns the number used to uniquely identify this account.
+ */
+ public String getNumber() {
+ return number;
+ }
+
+ /**
+ * Returns the name on file for this account.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Add a single beneficiary with a 100% allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ */
+ public void addBeneficiary(String beneficiaryName) {
+ addBeneficiary(beneficiaryName, Percentage.oneHundred());
+ }
+
+ /**
+ * Add a single beneficiary with the specified allocation percentage.
+ * @param beneficiaryName the name of the beneficiary (should be unique)
+ * @param allocationPercentage the beneficiary's allocation percentage within this account
+ */
+ public void addBeneficiary(String beneficiaryName, Percentage allocationPercentage) {
+ beneficiaries.add(new Beneficiary(beneficiaryName, allocationPercentage));
+ }
+
+ /**
+ * Validation check that returns true only if the total beneficiary allocation adds up to 100%.
+ */
+ public boolean isValid() {
+ Percentage totalPercentage = Percentage.zero();
+ for (Beneficiary b : beneficiaries) {
+ try {
+ totalPercentage = totalPercentage.add(b.getAllocationPercentage());
+ } catch (IllegalArgumentException e) {
+ // total would have been over 100% - return invalid
+ return false;
+ }
+ }
+ return totalPercentage.equals(Percentage.oneHundred());
+ }
+
+ /**
+ * Make a monetary contribution to this account. The contribution amount is distributed among the account's
+ * beneficiaries based on each beneficiary's allocation percentage.
+ * @param amount the total amount to contribute
+ */
+ public AccountContribution makeContribution(MonetaryAmount amount) {
+ if (!isValid()) {
+ throw new IllegalStateException(
+ "Cannot make contributions to this account: it has invalid beneficiary allocations");
+ }
+ Set distributions = distribute(amount);
+ return new AccountContribution(getNumber(), amount, distributions);
+ }
+
+ /**
+ * Distribute the contribution amount among this account's beneficiaries.
+ * @param amount the total contribution amount
+ * @return the individual beneficiary distributions
+ */
+ private Set distribute(MonetaryAmount amount) {
+ Set distributions = new HashSet<>(beneficiaries.size());
+ for (Beneficiary beneficiary : beneficiaries) {
+ MonetaryAmount distributionAmount = amount.multiplyBy(beneficiary.getAllocationPercentage());
+ beneficiary.credit(distributionAmount);
+ Distribution distribution = new Distribution(beneficiary.getName(), distributionAmount, beneficiary
+ .getAllocationPercentage(), beneficiary.getSavings());
+ distributions.add(distribution);
+ }
+ return distributions;
+ }
+
+ /**
+ * Returns the beneficiaries for this account. Callers should not attempt to hold on or modify the returned set.
+ * This method should only be used transitively; for example, called to facilitate account reporting.
+ * @return the beneficiaries of this account
+ */
+ public Set getBeneficiaries() {
+ return Collections.unmodifiableSet(beneficiaries);
+ }
+
+ /**
+ * Returns a single account beneficiary. Callers should not attempt to hold on or modify the returned object. This
+ * method should only be used transitively; for example, called to facilitate reporting or testing.
+ * @param name the name of the beneficiary e.g "Annabelle"
+ * @return the beneficiary object
+ */
+ public Beneficiary getBeneficiary(String name) {
+ for (Beneficiary b : beneficiaries) {
+ if (b.getName().equals(name)) {
+ return b;
+ }
+ }
+ throw new IllegalArgumentException("No such beneficiary with name '" + name + "'");
+ }
+
+ /**
+ * Used to restore an allocated beneficiary. Should only be called by the repository responsible for reconstituting
+ * this account.
+ * @param beneficiary the beneficiary
+ */
+ void restoreBeneficiary(Beneficiary beneficiary) {
+ beneficiaries.add(beneficiary);
+ }
+
+ public String toString() {
+ return "Number = '" + number + "', name = " + name + "', beneficiaries = " + beneficiaries;
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/AccountRepository.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/AccountRepository.java
new file mode 100644
index 0000000..16c6079
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/AccountRepository.java
@@ -0,0 +1,29 @@
+package rewards.internal.account;
+
+/**
+ * Loads account aggregates. Called by the reward network to find and reconstitute Account entities from an external
+ * form such as a set of RDMS rows.
+ *
+ * Objects returned by this repository are guaranteed to be fully-initialized and ready to use.
+ */
+public interface AccountRepository {
+
+ /**
+ * Load an account by its credit card.
+ * @param creditCardNumber the credit card number
+ * @return the account object
+ */
+ Account findByCreditCard(String creditCardNumber);
+
+ /**
+ * Updates the 'savings' of each account beneficiary. The new savings balance contains the amount distributed for a
+ * contribution made during a reward transaction.
+ *
+ * Note: use of an object-relational mapper (ORM) with support for transparent-persistence like Hibernate (or the
+ * new Java Persistence API (JPA)) would remove the need for this explicit update operation as the ORM would take
+ * care of applying relational updates to a modified Account entity automatically.
+ * @param account the account whose beneficiary savings have changed
+ */
+ void updateBeneficiaries(Account account);
+
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/Beneficiary.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/Beneficiary.java
new file mode 100644
index 0000000..647499b
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/Beneficiary.java
@@ -0,0 +1,79 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import common.repository.Entity;
+
+/**
+ * A single beneficiary allocated to an account. Each beneficiary has a name (e.g. Annabelle) and a savings balance
+ * tracking how much money has been saved for he or she to date (e.g. $1000).
+ */
+public class Beneficiary extends Entity {
+
+ private String name;
+
+ private Percentage allocationPercentage;
+
+ private MonetaryAmount savings = MonetaryAmount.valueOf("0.00");
+
+ @SuppressWarnings("unused")
+ private Beneficiary() {
+ }
+
+ /**
+ * Creates a new account beneficiary.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ */
+ public Beneficiary(String name, Percentage allocationPercentage) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ }
+
+ /**
+ * Creates a new account beneficiary. This constructor should be called by privileged objects responsible for
+ * reconstituting an existing Account object from some external form such as a collection of database records.
+ * Marked package-private to indicate this constructor should never be called by general application code.
+ * @param name the name of the beneficiary
+ * @param allocationPercentage the beneficiary's allocation percentage within its account
+ * @param savings the total amount saved to-date for this beneficiary
+ */
+ Beneficiary(String name, Percentage allocationPercentage, MonetaryAmount savings) {
+ this.name = name;
+ this.allocationPercentage = allocationPercentage;
+ this.savings = savings;
+ }
+
+ /**
+ * Returns the beneficiary name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Returns the beneficiary's allocation percentage in this account.
+ */
+ public Percentage getAllocationPercentage() {
+ return allocationPercentage;
+ }
+
+ /**
+ * Returns the amount of savings this beneficiary has accrued.
+ */
+ public MonetaryAmount getSavings() {
+ return savings;
+ }
+
+ /**
+ * Credit the amount to this beneficiary's saving balance.
+ * @param amount the amount to credit
+ */
+ public void credit(MonetaryAmount amount) {
+ savings = savings.add(amount);
+ }
+
+ public String toString() {
+ return "name = '" + name + "', allocationPercentage = " + allocationPercentage + ", savings = " + savings + ")";
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/JdbcAccountRepository.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/JdbcAccountRepository.java
new file mode 100644
index 0000000..33392fd
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/JdbcAccountRepository.java
@@ -0,0 +1,90 @@
+package rewards.internal.account;
+
+import common.money.MonetaryAmount;
+import common.money.Percentage;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.ResultSetExtractor;
+
+import javax.sql.DataSource;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Loads accounts from a data source using the JDBC API.
+ */
+public class JdbcAccountRepository implements AccountRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public JdbcAccountRepository(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ /**
+ * Extracts an Account object from rows returned from a join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY.
+ */
+ private final ResultSetExtractor accountExtractor = new AccountExtractor();
+
+ public Account findByCreditCard(String creditCardNumber) {
+ String sql = "select a.ID as ID, a.NUMBER as ACCOUNT_NUMBER, a.NAME as ACCOUNT_NAME, c.NUMBER as CREDIT_CARD_NUMBER, b.NAME as BENEFICIARY_NAME, b.ALLOCATION_PERCENTAGE as BENEFICIARY_ALLOCATION_PERCENTAGE, b.SAVINGS as BENEFICIARY_SAVINGS from T_ACCOUNT a, T_ACCOUNT_BENEFICIARY b, T_ACCOUNT_CREDIT_CARD c where ID = b.ACCOUNT_ID and ID = c.ACCOUNT_ID and c.NUMBER = ?";
+ return jdbcTemplate.query(sql, accountExtractor, creditCardNumber);
+ }
+
+ public void updateBeneficiaries(Account account) {
+ String sql = "update T_ACCOUNT_BENEFICIARY SET SAVINGS = ? where ACCOUNT_ID = ? and NAME = ?";
+ for (Beneficiary b : account.getBeneficiaries()) {
+ jdbcTemplate.update(sql, b.getSavings().asBigDecimal(), account.getEntityId(), b.getName());
+ }
+ }
+
+ /**
+ * Map the rows returned from the join of T_ACCOUNT and T_ACCOUNT_BENEFICIARY to a fully-reconstituted Account
+ * aggregate.
+ *
+ * @param rs the set of rows returned from the query
+ * @return the mapped Account aggregate
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Account mapAccount(ResultSet rs) throws SQLException {
+ Account account = null;
+ while (rs.next()) {
+ if (account == null) {
+ String number = rs.getString("ACCOUNT_NUMBER");
+ String name = rs.getString("ACCOUNT_NAME");
+ account = new Account(number, name);
+ // set internal entity identifier (primary key)
+ account.setEntityId(rs.getLong("ID"));
+ }
+ account.restoreBeneficiary(mapBeneficiary(rs));
+ }
+ if (account == null) {
+ // no rows returned - throw an empty result exception
+ throw new EmptyResultDataAccessException(1);
+ }
+ return account;
+ }
+
+ /**
+ * Maps the beneficiary columns in a single row to an AllocatedBeneficiary object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ * @return an allocated beneficiary
+ * @throws SQLException an exception occurred extracting data from the result set
+ */
+ private Beneficiary mapBeneficiary(ResultSet rs) throws SQLException {
+ String name = rs.getString("BENEFICIARY_NAME");
+ MonetaryAmount savings = MonetaryAmount.valueOf(rs.getString("BENEFICIARY_SAVINGS"));
+ Percentage allocationPercentage = Percentage.valueOf(rs.getString("BENEFICIARY_ALLOCATION_PERCENTAGE"));
+ return new Beneficiary(name, allocationPercentage, savings);
+ }
+
+ private class AccountExtractor implements ResultSetExtractor {
+
+ public Account extractData(ResultSet rs) throws SQLException, DataAccessException {
+ return mapAccount(rs);
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/package.html b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/package.html
new file mode 100644
index 0000000..9c20aa3
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/account/package.html
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
new file mode 100644
index 0000000..b7d6d74
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/restaurant/BenefitAvailabilityPolicy.java
@@ -0,0 +1,20 @@
+package rewards.internal.restaurant;
+
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+/**
+ * Determines if benefit is available for an account for dining.
+ *
+ * A value object. A strategy. Scoped by the Resturant aggregate.
+ */
+public interface BenefitAvailabilityPolicy {
+
+ /**
+ * Calculates if an account is eligible to receive benefits for a dining.
+ * @param account the account of the member who dined
+ * @param dining the dining event
+ * @return benefit availability status
+ */
+ boolean isBenefitAvailableFor(Account account, Dining dining);
+}
diff --git a/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
new file mode 100644
index 0000000..62f11a6
--- /dev/null
+++ b/lab/32-jdbc-autoconfig/src/main/java/rewards/internal/restaurant/JdbcRestaurantRepository.java
@@ -0,0 +1,114 @@
+package rewards.internal.restaurant;
+
+import common.money.Percentage;
+import org.springframework.jdbc.core.JdbcTemplate;
+import org.springframework.jdbc.core.RowMapper;
+import rewards.Dining;
+import rewards.internal.account.Account;
+
+import javax.sql.DataSource;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+/**
+ * Loads restaurants from a data source using the JDBC API.
+ */
+public class JdbcRestaurantRepository implements RestaurantRepository {
+
+ private final JdbcTemplate jdbcTemplate;
+
+ public JdbcRestaurantRepository(DataSource dataSource) {
+ this.jdbcTemplate = new JdbcTemplate(dataSource);
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ */
+ private final RowMapper rowMapper = new RestaurantRowMapper();
+
+ public Restaurant findByMerchantNumber(String merchantNumber) {
+ String sql = "select MERCHANT_NUMBER, NAME, BENEFIT_PERCENTAGE, BENEFIT_AVAILABILITY_POLICY from T_RESTAURANT where MERCHANT_NUMBER = ?";
+ return jdbcTemplate.queryForObject(sql, rowMapper, merchantNumber);
+ }
+
+ /**
+ * Maps a row returned from a query of T_RESTAURANT to a Restaurant object.
+ *
+ * @param rs the result set with its cursor positioned at the current row
+ */
+ private Restaurant mapRestaurant(ResultSet rs) throws SQLException {
+ // get the row column data
+ String name = rs.getString("NAME");
+ String number = rs.getString("MERCHANT_NUMBER");
+ Percentage benefitPercentage = Percentage.valueOf(rs.getString("BENEFIT_PERCENTAGE"));
+ // map to the object
+ Restaurant restaurant = new Restaurant(number, name);
+ restaurant.setBenefitPercentage(benefitPercentage);
+ restaurant.setBenefitAvailabilityPolicy(mapBenefitAvailabilityPolicy(rs));
+ return restaurant;
+ }
+
+ /**
+ * Helper method that maps benefit availability policy data in the ResultSet to a fully-configured
+ * {@link BenefitAvailabilityPolicy} object. The key column is 'BENEFIT_AVAILABILITY_POLICY', which is a
+ * discriminator column containing a string code that identifies the type of policy. Currently supported types are:
+ * 'A' for 'always available' and 'N' for 'never available'.
+ *
+ *