diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/.editorconfig b/exercises/tiered_pricing/solutions/adrianliz/java/.editorconfig new file mode 100644 index 00000000..219e4cd1 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.java] +indent_size = 4 +indent_style = space diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/.github/workflows/workflow.yml b/exercises/tiered_pricing/solutions/adrianliz/java/.github/workflows/workflow.yml new file mode 100644 index 00000000..38c6ded2 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/.github/workflows/workflow.yml @@ -0,0 +1,22 @@ +name: Main Workflow + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + + - uses: actions/setup-java@v1 + with: + java-version: 17 + + - name: Assemble + run: ./gradlew assemble --warning-mode all + + - name: Check + run: ./gradlew check --warning-mode all + diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/.gitignore b/exercises/tiered_pricing/solutions/adrianliz/java/.gitignore new file mode 100644 index 00000000..165da6e1 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/.gitignore @@ -0,0 +1,9 @@ +# Gradle +.gradle +build/ + +# Ignore Gradle GUI config +gradle-app.setting + +var/log/* +!var/log/.gitkeep diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/Makefile b/exercises/tiered_pricing/solutions/adrianliz/java/Makefile new file mode 100644 index 00000000..9bd15395 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/Makefile @@ -0,0 +1,10 @@ +.PHONY: all +all: build + +.PHONY: build +build: + @./gradlew assemble --warning-mode all + +.PHONY: test +test: + @./gradlew check --warning-mode all diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/README.md b/exercises/tiered_pricing/solutions/adrianliz/java/README.md new file mode 100644 index 00000000..fd0ee890 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/README.md @@ -0,0 +1,7 @@ +# Tiered pricing: Java + +Java codebase for [Tiered pricing](../../README.md) exercise. + +# Testing + +Run all tests with `make test` diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/build.gradle b/exercises/tiered_pricing/solutions/adrianliz/java/build.gradle new file mode 100644 index 00000000..a60b0294 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/build.gradle @@ -0,0 +1,37 @@ +plugins { + id 'org.springframework.boot' version '2.6.3' + id 'io.spring.dependency-management' version '1.0.11.RELEASE' + id 'java' +} + +group = 'tv.codely' + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.apache.logging.log4j:log4j-core:2.17.1' + implementation 'com.vlkan.log4j2:log4j2-logstash-layout:1.0.5' + implementation 'org.springframework.boot:spring-boot-starter-web:2.6.4' + implementation 'org.json:json:20211205' + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.4' + testImplementation 'com.github.javafaker:javafaker:1.0.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' +} + +test { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed" + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/gradle/wrapper/gradle-wrapper.jar b/exercises/tiered_pricing/solutions/adrianliz/java/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..87b738cb Binary files /dev/null and b/exercises/tiered_pricing/solutions/adrianliz/java/gradle/wrapper/gradle-wrapper.jar differ diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/gradle/wrapper/gradle-wrapper.properties b/exercises/tiered_pricing/solutions/adrianliz/java/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..e750102e --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/gradlew b/exercises/tiered_pricing/solutions/adrianliz/java/gradlew new file mode 100755 index 00000000..af6708ff --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/gradlew.bat b/exercises/tiered_pricing/solutions/adrianliz/java/gradlew.bat new file mode 100755 index 00000000..6d57edc7 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/InvalidSubscriptionTierPrice.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/InvalidSubscriptionTierPrice.java new file mode 100644 index 00000000..091da845 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/InvalidSubscriptionTierPrice.java @@ -0,0 +1,8 @@ +package tv.codely.checkout; + +public final class InvalidSubscriptionTierPrice extends RuntimeException { + + public InvalidSubscriptionTierPrice(final String message) { + super(message); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/InvalidSubscriptionTierRange.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/InvalidSubscriptionTierRange.java new file mode 100644 index 00000000..bc831903 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/InvalidSubscriptionTierRange.java @@ -0,0 +1,8 @@ +package tv.codely.checkout; + +public final class InvalidSubscriptionTierRange extends RuntimeException { + + public InvalidSubscriptionTierRange(final String message) { + super(message); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/InvalidSubscriptionTiers.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/InvalidSubscriptionTiers.java new file mode 100644 index 00000000..da34be48 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/InvalidSubscriptionTiers.java @@ -0,0 +1,8 @@ +package tv.codely.checkout; + +public final class InvalidSubscriptionTiers extends RuntimeException { + + public InvalidSubscriptionTiers(String message) { + super(message); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTier.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTier.java new file mode 100644 index 00000000..2aac8e00 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTier.java @@ -0,0 +1,54 @@ +package tv.codely.checkout; + +import java.util.Objects; + +public final class SubscriptionTier { + + private final SubscriptionTierRange range; + private final SubscriptionTierPrice price; + + public SubscriptionTier( + final SubscriptionTierRange range, + final SubscriptionTierPrice price) { + + this.range = range; + this.price = price; + } + + public boolean isFirst() { + return range.isFirst(); + } + + public boolean isLast() { + return range.isLast(); + } + + public boolean isInRange(int numberOfSubscriptions) { + return this.range.isSuitableFor(numberOfSubscriptions); + } + + public double unitPrice() { + return price.unitPrice(); + } + + public double getTotalPrice(int numberOfSubscriptions) { + return unitPrice() * numberOfSubscriptions; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SubscriptionTier that = (SubscriptionTier) o; + return Objects.equals(range, that.range) && Objects.equals(price, that.price); + } + + @Override + public int hashCode() { + return Objects.hash(range, price); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTierNotFound.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTierNotFound.java new file mode 100644 index 00000000..d6ac0d9e --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTierNotFound.java @@ -0,0 +1,9 @@ +package tv.codely.checkout; + +public final class SubscriptionTierNotFound extends RuntimeException { + + public SubscriptionTierNotFound(int numberOfSubscriptions) { + super(String.format("Subscription tier not found for %d numberOfSubscriptions", + numberOfSubscriptions)); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTierPrice.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTierPrice.java new file mode 100644 index 00000000..7e9024fd --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTierPrice.java @@ -0,0 +1,40 @@ +package tv.codely.checkout; + +import java.util.Objects; + +public final class SubscriptionTierPrice { + + private final double value; + + public SubscriptionTierPrice(double value) { + validate(value); + this.value = value; + } + + private static void validate(double value) { + if (value < 1) { + throw new InvalidSubscriptionTierPrice("The price cannot be negative"); + } + } + + public double unitPrice() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SubscriptionTierPrice that = (SubscriptionTierPrice) o; + return Double.compare(that.value, value) == 0; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTierRange.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTierRange.java new file mode 100644 index 00000000..d67c1342 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTierRange.java @@ -0,0 +1,88 @@ +package tv.codely.checkout; + +import java.util.Objects; + +// [from, to] +public final class SubscriptionTierRange { + + private final int numberOfSubscriptionsFrom; + private final int numberOfSubscriptionsTo; + + private SubscriptionTierRange(int numberOfSubscriptionsFrom, int numberOfSubscriptionsTo) { + this.numberOfSubscriptionsFrom = numberOfSubscriptionsFrom; + this.numberOfSubscriptionsTo = numberOfSubscriptionsTo; + } + + public static SubscriptionTierRange first(int numberOfSubscriptions) { + validate(numberOfSubscriptions); + return new SubscriptionTierRange(1, numberOfSubscriptions); + } + + public static SubscriptionTierRange last(final SubscriptionTierRange range) { + validate(range); + return new SubscriptionTierRange(range.numberOfSubscriptionsTo + 1, Integer.MAX_VALUE); + } + + public static SubscriptionTierRange from( + final SubscriptionTierRange range, + final int numberOfSubscriptions) { + + validate(range); + validate(numberOfSubscriptions); + + final int numberOfSubscriptionsFrom = range.numberOfSubscriptionsTo + 1; + final int numberOfSubscriptionsTo = numberOfSubscriptionsFrom + numberOfSubscriptions; + return new SubscriptionTierRange(numberOfSubscriptionsFrom, numberOfSubscriptionsTo); + } + + + private static void validate(final int numberOfSubscriptions) { + if (numberOfSubscriptions < 1) { + throw new InvalidSubscriptionTierRange( + "Number of subscriptions must be greater than 0"); + } + } + + private static void validate(final SubscriptionTierRange range) { + if (range == null) { + throw new InvalidSubscriptionTierRange( + "Cannot create a new subscription tier range from a null one"); + } + + if (range.isLast()) { + throw new InvalidSubscriptionTierRange( + "Cannot create a new subscription tier range after the last one"); + } + } + + public boolean isFirst() { + return numberOfSubscriptionsFrom == 1; + } + + public boolean isLast() { + return numberOfSubscriptionsTo == Integer.MAX_VALUE; + } + + public boolean isSuitableFor(int numberOfSubscriptions) { + return numberOfSubscriptions >= numberOfSubscriptionsFrom + && numberOfSubscriptions <= numberOfSubscriptionsTo; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + SubscriptionTierRange that = (SubscriptionTierRange) o; + return numberOfSubscriptionsFrom == that.numberOfSubscriptionsFrom + && numberOfSubscriptionsTo == that.numberOfSubscriptionsTo; + } + + @Override + public int hashCode() { + return Objects.hash(numberOfSubscriptionsFrom, numberOfSubscriptionsTo); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTiers.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTiers.java new file mode 100644 index 00000000..0cc8c755 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/SubscriptionTiers.java @@ -0,0 +1,42 @@ +package tv.codely.checkout; + +import java.util.Set; + +public final class SubscriptionTiers { + + private final Set tiers; + + public SubscriptionTiers(final Set tiers) { + validate(tiers); + this.tiers = tiers; + } + + private static void validate(final Set tiers) { + if (tiers == null || tiers.isEmpty()) { + throw new InvalidSubscriptionTiers("There must be at least one subscription tier"); + } + + if (tiers.stream().noneMatch(SubscriptionTier::isFirst)) { + throw new InvalidSubscriptionTiers("There must be a first subscription tier"); + } + + if (tiers.stream().noneMatch(SubscriptionTier::isLast)) { + throw new InvalidSubscriptionTiers("There must be a last subscription tier"); + } + } + + private SubscriptionTier findSuitableTier(int numberOfSubscriptions) { + return tiers.stream() + .filter(tier -> tier.isInRange(numberOfSubscriptions)) + .findFirst() + .orElseThrow(() -> new SubscriptionTierNotFound(numberOfSubscriptions)); + } + + public double getTotalPrice(int numberOfSubscriptions) { + return findSuitableTier(numberOfSubscriptions).getTotalPrice(numberOfSubscriptions); + } + + public double getBasePrice(int numberOfSubscriptions) { + return findSuitableTier(numberOfSubscriptions).unitPrice(); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/TieredPricing.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/TieredPricing.java new file mode 100644 index 00000000..d80e5a72 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/TieredPricing.java @@ -0,0 +1,24 @@ +package tv.codely.checkout; + +import java.util.Set; + +public class TieredPricing { + + private final SubscriptionTiers tiers; + + public TieredPricing(final SubscriptionTiers subscriptionTiers) { + this.tiers = subscriptionTiers; + } + + public TieredPricing(final Set subscriptionTiers) { + this(new SubscriptionTiers(subscriptionTiers)); + } + + public double getTotalPrice(int numberOfSubscriptions) { + return tiers.getTotalPrice(numberOfSubscriptions); + } + + public double getBasePrice(int numberOfSubscriptions) { + return tiers.getBasePrice(numberOfSubscriptions); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/api/CheckoutApplication.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/api/CheckoutApplication.java new file mode 100644 index 00000000..89c23ee9 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/api/CheckoutApplication.java @@ -0,0 +1,11 @@ +package tv.codely.checkout.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class CheckoutApplication { + public static void main(String[] args) { + SpringApplication.run(CheckoutApplication.class, args); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/api/Controller/ExampleGetController.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/api/Controller/ExampleGetController.java new file mode 100644 index 00000000..15b2681e --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/java/tv/codely/checkout/api/Controller/ExampleGetController.java @@ -0,0 +1,21 @@ +package tv.codely.checkout.api.Controller; + +import org.json.JSONException; +import org.json.JSONObject; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; + +@RestController +public class ExampleGetController { + + @GetMapping("/") + public ResponseEntity response(@RequestParam String name, HttpServletResponse response) throws JSONException { + response.addHeader("content-type", "application/json"); + + return ResponseEntity.ok(new JSONObject().put("hello", name).toString()); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/main/resources/log4j2.properties b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/resources/log4j2.properties new file mode 100644 index 00000000..1d009f67 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/main/resources/log4j2.properties @@ -0,0 +1,34 @@ +name = CodelyTvJavaBasicSkeleton +property.filename = logs +appenders = console, file + +status = warn + +appender.console.name = CONSOLE +appender.console.type = CONSOLE +appender.console.target = SYSTEM_OUT + +appender.console.logstash.type = LogstashLayout +appender.console.logstash.dateTimeFormatPattern = yyyy-MM-dd'T'HH:mm:ss.SSSZZZ +appender.console.logstash.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json +appender.console.logstash.prettyPrintEnabled = true +appender.console.logstash.stackTraceEnabled = true + +appender.file.type = File +appender.file.name = LOGFILE +appender.file.fileName=var/log/java-basic-skeleton.log +appender.file.logstash.type=LogstashLayout +appender.file.logstash.dateTimeFormatPattern = yyyy-MM-dd'T'HH:mm:ss.SSSZZZ +appender.file.logstash.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json +appender.file.logstash.prettyPrintEnabled = false +appender.file.logstash.stackTraceEnabled = true + +loggers = file +logger.file.name = tv.codely.java_basic_skeleton +logger.file.level = info +logger.file.appenderRefs = file +logger.file.appenderRef.file.ref = LOGFILE + +rootLogger.level = info +rootLogger.appenderRefs = stdout +rootLogger.appenderRef.stdout.ref = CONSOLE diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/SubscriptionTierPriceShould.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/SubscriptionTierPriceShould.java new file mode 100644 index 00000000..6b9725f8 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/SubscriptionTierPriceShould.java @@ -0,0 +1,13 @@ +package tv.codely.checkout; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public final class SubscriptionTierPriceShould { + + @Test + void throw_invalid_subscription_tier_price_if_price_is_less_than_1() { + assertThrows(InvalidSubscriptionTierPrice.class, () -> new SubscriptionTierPrice(0)); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/SubscriptionTierRangeShould.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/SubscriptionTierRangeShould.java new file mode 100644 index 00000000..1ba0df70 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/SubscriptionTierRangeShould.java @@ -0,0 +1,19 @@ +package tv.codely.checkout; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.Test; + +public final class SubscriptionTierRangeShould { + + @Test + void throw_invalid_subscription_tier_range_if_number_of_subscriptions_is_less_than_1() { + assertThrows(InvalidSubscriptionTierRange.class, () -> SubscriptionTierRange.first(0)); + } + + @Test + void throw_invalid_subscription_tier_range_if_previous_range_does_not_exist() { + assertThrows(InvalidSubscriptionTierRange.class, () -> SubscriptionTierRange.last(null)); + } + +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/TieredPricingShould.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/TieredPricingShould.java new file mode 100644 index 00000000..b14529fb --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/TieredPricingShould.java @@ -0,0 +1,118 @@ +package tv.codely.checkout; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Set; +import org.junit.jupiter.api.Test; +import tv.codely.checkout.mother.IntegerMother; +import tv.codely.checkout.mother.SubscriptionTierMother; +import tv.codely.checkout.mother.SubscriptionTierPriceMother; +import tv.codely.checkout.mother.SubscriptionTierRangeMother; + +public class TieredPricingShould { + + private static Set defaultSubscriptionTiers() { + final var firstTierRange = SubscriptionTierRange.first(2); + final var secondTierRange = SubscriptionTierRange.from(firstTierRange, 7); + final var thirdTierRange = SubscriptionTierRange.from(secondTierRange, 14); + final var fourthTierRange = SubscriptionTierRange.from(thirdTierRange, 24); + final var lastTierRange = SubscriptionTierRange.last(fourthTierRange); + return Set.of( + new SubscriptionTier( + firstTierRange, + new SubscriptionTierPrice(299)), + new SubscriptionTier( + secondTierRange, + new SubscriptionTierPrice(239)), + new SubscriptionTier( + thirdTierRange, + new SubscriptionTierPrice(219)), + new SubscriptionTier( + fourthTierRange, + new SubscriptionTierPrice(199)), + new SubscriptionTier( + lastTierRange, + new SubscriptionTierPrice(149))); + } + + @Test + void return_total_price_based_on_number_of_subscriptions() { + final var subscriptionTiers = SubscriptionTierMother.randoms(); + final var tieredPricing = new TieredPricing(subscriptionTiers); + final var numberOfSubscriptions = IntegerMother.random(); + final var expectedPrice = + tieredPricing.getBasePrice(numberOfSubscriptions) * numberOfSubscriptions; + + final var totalPrice = tieredPricing.getTotalPrice(numberOfSubscriptions); + + assertEquals(expectedPrice, totalPrice); + } + + @Test + void return_total_price_for_1_subscription() { + final var tieredPricing = new TieredPricing(defaultSubscriptionTiers()); + final var expectedPrice = 299; + + final var totalPrice = tieredPricing.getTotalPrice(1); + assertEquals(expectedPrice, totalPrice); + } + + @Test + void return_total_price_for_3_subscriptions() { + final var tieredPricing = new TieredPricing(defaultSubscriptionTiers()); + final var expectedPrice = 239 * 3; + + final var totalPrice = tieredPricing.getTotalPrice(3); + assertEquals(expectedPrice, totalPrice); + } + + @Test + void return_total_price_for_11_subscriptions() { + final var tieredPricing = new TieredPricing(defaultSubscriptionTiers()); + final var expectedPrice = 219 * 11; + + final var totalPrice = tieredPricing.getTotalPrice(11); + assertEquals(expectedPrice, totalPrice); + } + + @Test + void return_total_price_for_26_subscriptions() { + final var tieredPricing = new TieredPricing(defaultSubscriptionTiers()); + final var expectedPrice = 199 * 26; + + final var totalPrice = tieredPricing.getTotalPrice(26); + assertEquals(expectedPrice, totalPrice); + } + + @Test + void return_total_price_for_51_subscriptions() { + final var tieredPricing = new TieredPricing(defaultSubscriptionTiers()); + final var expectedPrice = 149 * 51; + + final var totalPrice = tieredPricing.getTotalPrice(51); + assertEquals(expectedPrice, totalPrice); + } + + @Test + void throw_invalid_subscription_tiers_if_there_is_no_tiers() { + assertThrows(InvalidSubscriptionTiers.class, () -> new TieredPricing(Set.of())); + } + + @Test + void throw_invalid_subscription_tiers_if_there_is_no_last_tier() { + final var subscriptionTiers = + Set.of(SubscriptionTierMother.create(SubscriptionTierRangeMother.first(10), + SubscriptionTierPriceMother.random())); + + assertThrows(InvalidSubscriptionTiers.class, () -> new TieredPricing(subscriptionTiers)); + } + + @Test + void throw_subscription_tier_not_found_if_number_of_subscriptions_does_not_match_any_tier() { + final var tieredPricing = new TieredPricing(defaultSubscriptionTiers()); + final var numberOfSubscriptions = 0; + assertThrows(SubscriptionTierNotFound.class, + () -> tieredPricing.getTotalPrice(numberOfSubscriptions)); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/api/ApiTestCase.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/api/ApiTestCase.java new file mode 100644 index 00000000..e0346b69 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/api/ApiTestCase.java @@ -0,0 +1,67 @@ +package tv.codely.checkout.api; + +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.ResultActions; +import org.springframework.test.web.servlet.ResultMatcher; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest +public abstract class ApiTestCase { + + @Autowired + private MockMvc mockMvc; + private ResultActions currentRequest; + + + protected void assertJsonResponse( + String endpoint, + Integer expectedStatusCode, + String expectedResponse + ) throws Exception { + ResultMatcher response = expectedResponse.isEmpty() + ? content().string("") + : content().json(expectedResponse); + + mockMvc + .perform(get(endpoint)) + .andExpect(status().is(expectedStatusCode)) + .andExpect(response) + .andExpect(header().string("content-type", "application/json")); + + } + + protected void whenGetRequestSentTo(String endpoint) throws Exception { + currentRequest = mockMvc.perform(get(endpoint)); + } + + protected void assertStatusCodeIs(Integer expectedStatusCode) throws Exception { + currentRequest.andExpect(status().is(expectedStatusCode)); + } + + protected void assertContentTypeIsA(String expectedContentType) throws Exception { + currentRequest.andExpect(header().string("content-type", expectedContentType)); + } + + protected void assertResponseIs(String expectedResponse) throws Exception { + ResultMatcher response = expectedResponse.isEmpty() + ? content().string("") + : content().json(expectedResponse); + + currentRequest.andExpect(response); + } + + protected void assertSuccessJsonResponse(JSONObject json) throws Exception { + assertJsonResponse(200, json); + } + + protected void assertJsonResponse(int statusCode, JSONObject json) throws Exception { + assertStatusCodeIs(statusCode); + assertContentTypeIsA("application/json"); + assertResponseIs(json.toString()); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/api/Controller/ExampleGetControllerShould.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/api/Controller/ExampleGetControllerShould.java new file mode 100644 index 00000000..55c7a3b0 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/api/Controller/ExampleGetControllerShould.java @@ -0,0 +1,16 @@ +package tv.codely.checkout.api.Controller; + +import org.json.JSONObject; +import org.junit.jupiter.api.Test; +import tv.codely.checkout.api.ApiTestCase; + +public class ExampleGetControllerShould extends ApiTestCase { + + @Test + public void spring_boot_e2e_test_example() throws Exception { + whenGetRequestSentTo("/?name=world"); + + JSONObject jsonResponse = new JSONObject().put("hello", "world"); + assertSuccessJsonResponse(jsonResponse); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/DoubleMother.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/DoubleMother.java new file mode 100644 index 00000000..f9441291 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/DoubleMother.java @@ -0,0 +1,8 @@ +package tv.codely.checkout.mother; + +public final class DoubleMother { + + public static double randomBetween(final int min, final int max) { + return MotherCreator.random().number().randomDouble(2, min, max); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/IntegerMother.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/IntegerMother.java new file mode 100644 index 00000000..38ce43ea --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/IntegerMother.java @@ -0,0 +1,12 @@ +package tv.codely.checkout.mother; + +public final class IntegerMother { + + public static int random() { + return MotherCreator.random().number().randomDigitNotZero(); + } + + public static int randomBetween(final int min, final int max) { + return MotherCreator.random().number().numberBetween(min, max); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/MotherCreator.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/MotherCreator.java new file mode 100644 index 00000000..d3c5bd76 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/MotherCreator.java @@ -0,0 +1,12 @@ +package tv.codely.checkout.mother; + +import com.github.javafaker.Faker; + +public final class MotherCreator { + + private static final Faker faker = new Faker(); + + public static Faker random() { + return faker; + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/SubscriptionTierMother.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/SubscriptionTierMother.java new file mode 100644 index 00000000..593d77d9 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/SubscriptionTierMother.java @@ -0,0 +1,34 @@ +package tv.codely.checkout.mother; + +import java.util.Set; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import tv.codely.checkout.SubscriptionTier; +import tv.codely.checkout.SubscriptionTierPrice; +import tv.codely.checkout.SubscriptionTierRange; + +public final class SubscriptionTierMother { + + public static SubscriptionTier create( + final SubscriptionTierRange range, + final SubscriptionTierPrice price) { + + return new SubscriptionTier(range, price); + } + + public static Set randoms() { + final var subscriptionTierRanges = SubscriptionTierRangeMother.randoms(); + final var previousSubscriptionTierPrice = new AtomicReference<>(299D); + + return subscriptionTierRanges.stream() + .map(range -> { + final var subscriptionTierPrice = + DoubleMother.randomBetween( + previousSubscriptionTierPrice.get().intValue() - 60, + previousSubscriptionTierPrice.get().intValue() - 20); + previousSubscriptionTierPrice.set(subscriptionTierPrice); + return create(range, SubscriptionTierPriceMother.create(subscriptionTierPrice)); + }) + .collect(Collectors.toSet()); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/SubscriptionTierPriceMother.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/SubscriptionTierPriceMother.java new file mode 100644 index 00000000..a2e3920b --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/SubscriptionTierPriceMother.java @@ -0,0 +1,14 @@ +package tv.codely.checkout.mother; + +import tv.codely.checkout.SubscriptionTierPrice; + +public final class SubscriptionTierPriceMother { + + public static SubscriptionTierPrice create(final double value) { + return new SubscriptionTierPrice(value); + } + + public static SubscriptionTierPrice random() { + return create(DoubleMother.randomBetween(100, 300)); + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/SubscriptionTierRangeMother.java b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/SubscriptionTierRangeMother.java new file mode 100644 index 00000000..bf8e5fae --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/java/tv/codely/checkout/mother/SubscriptionTierRangeMother.java @@ -0,0 +1,44 @@ +package tv.codely.checkout.mother; + +import java.util.ArrayList; +import java.util.List; +import tv.codely.checkout.SubscriptionTierRange; + +public final class SubscriptionTierRangeMother { + + public static SubscriptionTierRange first(final int numberOfSubscriptions) { + return SubscriptionTierRange.first(numberOfSubscriptions); + } + + public static SubscriptionTierRange last(final SubscriptionTierRange range) { + return SubscriptionTierRange.last(range); + } + + public static List randoms() { + final var tierRanges = new ArrayList(); + final var numberOfTiers = IntegerMother.randomBetween(1, 6); + final var minSubscriptionsInTier = 3; + final var maxSubscriptionsInTier = + IntegerMother.randomBetween(minSubscriptionsInTier + 1, 20); + var currentTierRange = first( + IntegerMother.randomBetween(minSubscriptionsInTier, maxSubscriptionsInTier)); + tierRanges.add(currentTierRange); + + for (int i = 0; i < numberOfTiers; i++) { + if (i == numberOfTiers - 1) { + tierRanges.add(last(currentTierRange)); + break; + } + + final var numberOfSubscriptions = + IntegerMother.randomBetween(minSubscriptionsInTier, maxSubscriptionsInTier); + final var tierRange = SubscriptionTierRange.from(currentTierRange, + numberOfSubscriptions); + + currentTierRange = tierRange; + tierRanges.add(tierRange); + } + + return tierRanges; + } +} diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/src/test/resources/log4j2.properties b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/resources/log4j2.properties new file mode 100644 index 00000000..1aae4672 --- /dev/null +++ b/exercises/tiered_pricing/solutions/adrianliz/java/src/test/resources/log4j2.properties @@ -0,0 +1,33 @@ +name = CodelyTvJavaBasicSkeleton +property.filename = logs +appenders = console, file + +status = warn + +appender.console.name = CONSOLE +appender.console.type = CONSOLE +appender.console.target = SYSTEM_OUT + +appender.console.layout.type = PatternLayout +appender.console.layout.pattern = [%level] [%date{HH:mm:ss.SSS}] [%class{0}#%method:%line] %message \(%mdc\) %n%throwable +appender.console.filter.threshold.type = ThresholdFilter +appender.console.filter.threshold.level = info + +appender.file.type = File +appender.file.name = LOGFILE +appender.file.fileName=var/log/java-basic-skeleton-test.log +appender.file.logstash.type=LogstashLayout +appender.file.logstash.dateTimeFormatPattern = yyyy-MM-dd'T'HH:mm:ss.SSSZZZ +appender.file.logstash.eventTemplateUri = classpath:LogstashJsonEventLayoutV1.json +appender.file.logstash.prettyPrintEnabled = false +appender.file.logstash.stackTraceEnabled = true + +loggers = file +logger.file.name = tv.codely.java_basic_skeleton +logger.file.level = info +logger.file.appenderRefs = file +logger.file.appenderRef.file.ref = LOGFILE + +rootLogger.level = info +rootLogger.appenderRefs = stdout +rootLogger.appenderRef.stdout.ref = CONSOLE diff --git a/exercises/tiered_pricing/solutions/adrianliz/java/var/log/.gitkeep b/exercises/tiered_pricing/solutions/adrianliz/java/var/log/.gitkeep new file mode 100644 index 00000000..e69de29b