diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..15abd7ac7b --- /dev/null +++ b/.gitignore @@ -0,0 +1,67 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +# Created by https://www.toptal.com/developers/gitignore/api/gradle +# Edit at https://www.toptal.com/developers/gitignore?templates=gradle + +### Gradle ### +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Avoid ignore Gradle wrappper properties +!gradle-wrapper.properties + +# Cache of project +.gradletasknamecache + +# Eclipse Gradle plugin generated files +# Eclipse Core +.project +# JDT-specific (Eclipse Java Development Tools) +.classpath + +### Gradle Patch ### +# Java heap dump +*.hprof + +# End of https://www.toptal.com/developers/gitignore/api/gradle + +logs/ \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..eb3061aa0d --- /dev/null +++ b/build.gradle @@ -0,0 +1,43 @@ +plugins { + id 'java-library' + id 'org.springframework.boot' version '3.1.4' + id 'io.spring.dependency-management' version '1.1.3' +} + +apply plugin: 'io.spring.dependency-management' + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' + implementation 'org.springframework.boot:spring-boot-starter-aop' + implementation 'com.fasterxml.jackson.core:jackson-databind' + implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-xml' + implementation 'org.codehaus.woodstox:woodstox-core-asl:4.4.1' + runtimeOnly 'mysql:mysql-connector-java:8.0.33' + implementation 'org.beryx:text-io:3.4.1' + testImplementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + testImplementation 'org.springframework.boot:spring-boot-starter-test' +} + +test { + useJUnitPlatform() +} + +group = 'com.programmers' +version = '0.0.1-SNAPSHOT' +description = 'voucherManagement' +java.sourceCompatibility = JavaVersion.VERSION_17 + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + +tasks.withType(Javadoc).configureEach { + options.encoding = 'UTF-8' +} \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..7f93135c49 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..ac72c34e8a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000000..0adc8e1a53 --- /dev/null +++ b/gradlew @@ -0,0 +1,249 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original 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 +# +# https://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. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# 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 ;; #( + MSYS* | 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 + if ! command -v java >/dev/null 2>&1 + then + 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 +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# 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" "-Xms64m"' + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..93e3f59f13 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,92 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@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=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@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" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +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 execute + +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 + +: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 %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 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! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..015d9795e7 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +/* + * This file was generated by the Gradle 'init' task. + */ + +rootProject.name = 'voucherManagement' diff --git a/src/main/java/com/programmers/vouchermanagement/VoucherManagementApplication.java b/src/main/java/com/programmers/vouchermanagement/VoucherManagementApplication.java new file mode 100644 index 0000000000..4da2f2d990 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/VoucherManagementApplication.java @@ -0,0 +1,16 @@ +package com.programmers.vouchermanagement; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.context.properties.ConfigurationPropertiesScan; + +@SpringBootApplication +@ConfigurationPropertiesScan +public class VoucherManagementApplication { + + public static void main(String[] args) { + var application = new SpringApplication(VoucherManagementApplication.class); + application.run(args); + } + +} diff --git a/src/main/java/com/programmers/vouchermanagement/configuration/ConsoleConfig.java b/src/main/java/com/programmers/vouchermanagement/configuration/ConsoleConfig.java new file mode 100644 index 0000000000..990450ed1d --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/configuration/ConsoleConfig.java @@ -0,0 +1,16 @@ +package com.programmers.vouchermanagement.configuration; + +import org.beryx.textio.TextIO; +import org.beryx.textio.TextIoFactory; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Profile("console") +@Configuration +public class ConsoleConfig { + @Bean + public TextIO textIO() { + return TextIoFactory.getTextIO(); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/configuration/FileConfig.java b/src/main/java/com/programmers/vouchermanagement/configuration/FileConfig.java new file mode 100644 index 0000000000..e0ab3f51d5 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/configuration/FileConfig.java @@ -0,0 +1,15 @@ +package com.programmers.vouchermanagement.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Profile("file") +@Configuration +public class FileConfig { + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/configuration/JdbcConfig.java b/src/main/java/com/programmers/vouchermanagement/configuration/JdbcConfig.java new file mode 100644 index 0000000000..2d233cb4f0 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/configuration/JdbcConfig.java @@ -0,0 +1,17 @@ +package com.programmers.vouchermanagement.configuration; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; + +import javax.sql.DataSource; + +@Profile("jdbc") +@Configuration +public class JdbcConfig { + @Bean + public NamedParameterJdbcTemplate namedParameterJdbcTemplate(DataSource dataSource) { + return new NamedParameterJdbcTemplate(dataSource); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/configuration/MvcConfig.java b/src/main/java/com/programmers/vouchermanagement/configuration/MvcConfig.java new file mode 100644 index 0000000000..68b95ced2e --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/configuration/MvcConfig.java @@ -0,0 +1,34 @@ +package com.programmers.vouchermanagement.configuration; + +import com.fasterxml.jackson.module.paramnames.ParameterNamesModule; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; +import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter; +import org.springframework.http.converter.xml.MappingJackson2XmlHttpMessageConverter; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +import java.text.SimpleDateFormat; +import java.util.List; + +@Profile({"thyme", "api"}) +@Configuration +public class MvcConfig implements WebMvcConfigurer { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/api/*") + .allowedOrigins("*"); + } + + @Override + public void configureMessageConverters(List> converters) { + Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder() + .indentOutput(true) + .dateFormat(new SimpleDateFormat("yyyy-MM-dd")) + .modulesToInstall(new ParameterNamesModule()); + converters.add(new MappingJackson2HttpMessageConverter(builder.build())); + converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build())); + } +} \ No newline at end of file diff --git a/src/main/java/com/programmers/vouchermanagement/consoleapp/io/ConsoleManager.java b/src/main/java/com/programmers/vouchermanagement/consoleapp/io/ConsoleManager.java new file mode 100644 index 0000000000..3f9ba8ded5 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/consoleapp/io/ConsoleManager.java @@ -0,0 +1,127 @@ +package com.programmers.vouchermanagement.consoleapp.io; + +import com.programmers.vouchermanagement.consoleapp.menu.Menu; +import com.programmers.vouchermanagement.customer.controller.dto.CustomerResponse; +import com.programmers.vouchermanagement.voucher.domain.vouchertype.VoucherType; +import com.programmers.vouchermanagement.voucher.domain.vouchertype.VoucherTypeManager; +import com.programmers.vouchermanagement.voucher.controller.dto.CreateVoucherRequest; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import org.beryx.textio.TextIO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Objects; + +import static com.programmers.vouchermanagement.util.Constant.LINE_SEPARATOR; + +@Profile("console") +@Component +public class ConsoleManager { + private static final Logger logger = LoggerFactory.getLogger(ConsoleManager.class); + //messages + private static final String MENU_SELECTION_INSTRUCTION = """ + === Voucher Program === + Type **exit** to exit the program. + Type **create** to create a new voucher. + Type **list** to list all vouchers. + Type **blacklist** to list all customers in blacklist. + """; + private static final String CREATE_SELECTION_INSTRUCTION = """ + Please select the type of voucher to create. + Type **fixed** to create a fixed amount voucher. + Type **percent** to create a percent discount voucher. + """; + private static final String VOUCHER_DISCOUNT_AMOUNT_INSTRUCTION = + "Please type the amount/percent of discount of the voucher.%s".formatted(LINE_SEPARATOR); + private static final String EXIT_MESSAGE = + "System exits."; + private static final String CREATE_SUCCESS_MESSAGE = + "The voucher(ID: %s) is successfully created."; + private static final String INCORRECT_INPUT_MESSAGE = + """ + Such input is incorrect. + Please input a correct command carefully. + """; + private static final String INVALID_VOUCHER_TYPE_MESSAGE = + "Voucher type should be either fixed amount or percent discount voucher."; + private static final String PERCENTAGE = " %"; + private static final String EMPTY = ""; + private static final String NO_CONTENT = "There is no %s stored yet!"; + //--- + + private final TextIO textIO; + + public ConsoleManager(TextIO textIO) { + this.textIO = textIO; + } + + public Menu selectMenu() { + String input = textIO.newStringInputReader() + .read(MENU_SELECTION_INSTRUCTION); + + return Menu.findMenu(input); + } + + public CreateVoucherRequest instructCreate() { + String createMenu = textIO.newStringInputReader() + .read(CREATE_SELECTION_INSTRUCTION); + VoucherType voucherType = VoucherTypeManager.get(createMenu); + + String discountValueStr = textIO.newStringInputReader() + .read(VOUCHER_DISCOUNT_AMOUNT_INSTRUCTION); + return new CreateVoucherRequest(voucherType.getName(), Long.parseLong(discountValueStr)); + } + + public void printCreateResult(VoucherResponse voucher) { + textIO.getTextTerminal().println(CREATE_SUCCESS_MESSAGE.formatted(voucher.id())); + } + + public void printReadAllVouchers(List vouchers) { + if (vouchers.isEmpty()) { + textIO.getTextTerminal().println(NO_CONTENT.formatted("voucher")); + } + vouchers.forEach(voucher -> textIO.getTextTerminal().println(formatVoucherDTO(voucher))); + } + + public void printReadBlacklist(List customers) { + if (customers.isEmpty()) { + textIO.getTextTerminal().println(NO_CONTENT.formatted("black customer")); + } + customers.forEach(customer -> textIO.getTextTerminal().println(formatCustomer(customer))); + } + + private String formatCustomer(CustomerResponse customer) { + return """ + Customer ID : %s + Customer Name : %s + -------------------------""" + .formatted(customer.id(), customer.name()); + } + + private String formatVoucherDTO(VoucherResponse voucher) { + return """ + Voucher ID : %s + Voucher Type : %s Discount Voucher + Discount Amount : %s + -------------------------""" + .formatted(voucher.id(), + voucher.typeName(), + voucher.discountValue() + + (Objects.equals(voucher.typeName(), "PERCENT") ? PERCENTAGE : EMPTY)); + } + + public void printExit() { + textIO.getTextTerminal().println(EXIT_MESSAGE); + } + + public void printIncorrectMenu() { + textIO.getTextTerminal().println(INCORRECT_INPUT_MESSAGE); + } + + public void printException(RuntimeException e) { + textIO.getTextTerminal().println(e.getMessage()); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/consoleapp/menu/Menu.java b/src/main/java/com/programmers/vouchermanagement/consoleapp/menu/Menu.java new file mode 100644 index 0000000000..f585eea062 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/consoleapp/menu/Menu.java @@ -0,0 +1,38 @@ +package com.programmers.vouchermanagement.consoleapp.menu; + +import java.util.Arrays; +import java.util.Objects; + +public enum Menu { + EXIT("exit"), + CREATE("create"), + LIST("list"), + BLACKLIST("blacklist"), + INCORRECT_MENU("incorrect menu"); + + private final String menuName; + + Menu(String menuName) { + this.menuName = menuName; + } + + //set static to tell that this method does not depend on a particular Menu value + public static Menu findMenu(String input) { + return Arrays.stream(Menu.values()) + .filter(menu -> menu.isMatching(input)) + .findFirst() + .orElse(INCORRECT_MENU); + } + + private boolean isMatching(String input) { + return Objects.equals(menuName, input); + } + + public boolean isExit() { + return this == Menu.EXIT; + } + + public boolean isIncorrect() { + return this == INCORRECT_MENU; + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/consoleapp/menu/MenuHandler.java b/src/main/java/com/programmers/vouchermanagement/consoleapp/menu/MenuHandler.java new file mode 100644 index 0000000000..28aa4c5391 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/consoleapp/menu/MenuHandler.java @@ -0,0 +1,83 @@ +package com.programmers.vouchermanagement.consoleapp.menu; + +import com.programmers.vouchermanagement.consoleapp.io.ConsoleManager; +import com.programmers.vouchermanagement.customer.controller.CustomerConsoleController; +import com.programmers.vouchermanagement.customer.controller.dto.CustomerResponse; +import com.programmers.vouchermanagement.voucher.controller.VoucherConsoleController; +import com.programmers.vouchermanagement.voucher.controller.dto.CreateVoucherRequest; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Profile("console") +@Component +public class MenuHandler { + private static final Logger logger = LoggerFactory.getLogger(MenuHandler.class); + + //messages + private static final String INCORRECT_MESSAGE = + "This menu is not executable."; + //--- + + private final ConsoleManager consoleManager; + private final VoucherConsoleController voucherController; + private final CustomerConsoleController customerController; + + public MenuHandler(ConsoleManager consoleManager, VoucherConsoleController voucherController, CustomerConsoleController customerController) { + this.consoleManager = consoleManager; + this.voucherController = voucherController; + this.customerController = customerController; + } + + public boolean handleMenu() { + Menu menu = selectMenu(); + + try { + executeMenu(menu); + } catch (RuntimeException e) { + consoleManager.printException(e); + } + + return isValidMenu(menu); + } + + private Menu selectMenu() { + return consoleManager.selectMenu(); + } + + private boolean isValidMenu(Menu menu) { + if (menu.isExit()) { + return false; + } + + if (menu.isIncorrect()) { + logger.error(INCORRECT_MESSAGE); + } + + return true; + } + + private void executeMenu(Menu menu) { + switch (menu) { + case EXIT -> consoleManager.printExit(); + case INCORRECT_MENU -> consoleManager.printIncorrectMenu(); + case CREATE -> { + CreateVoucherRequest createVoucherRequest = consoleManager.instructCreate(); + VoucherResponse voucher = voucherController.create(createVoucherRequest); + consoleManager.printCreateResult(voucher); + } + case LIST -> { + List vouchers = voucherController.readAll(); + consoleManager.printReadAllVouchers(vouchers); + } + case BLACKLIST -> { + List blackCustomers = customerController.readAllBlackCustomer(); + consoleManager.printReadBlacklist(blackCustomers); + } + } + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/consoleapp/runner/ConsoleAppRunner.java b/src/main/java/com/programmers/vouchermanagement/consoleapp/runner/ConsoleAppRunner.java new file mode 100644 index 0000000000..48954f9eff --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/consoleapp/runner/ConsoleAppRunner.java @@ -0,0 +1,25 @@ +package com.programmers.vouchermanagement.consoleapp.runner; + +import com.programmers.vouchermanagement.consoleapp.menu.MenuHandler; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Profile("console") +@Component +public class ConsoleAppRunner implements ApplicationRunner { + private final MenuHandler menuHandler; + + public ConsoleAppRunner(MenuHandler menuHandler) { + this.menuHandler = menuHandler; + } + + @Override + public void run(ApplicationArguments args) { + boolean isRunning = true; + while (isRunning) { + isRunning = menuHandler.handleMenu(); + } + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/customer/controller/CustomerConsoleController.java b/src/main/java/com/programmers/vouchermanagement/customer/controller/CustomerConsoleController.java new file mode 100644 index 0000000000..ee41d1709c --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/controller/CustomerConsoleController.java @@ -0,0 +1,31 @@ +package com.programmers.vouchermanagement.customer.controller; + +import com.programmers.vouchermanagement.customer.controller.dto.CreateCustomerRequest; +import com.programmers.vouchermanagement.customer.controller.dto.CustomerResponse; +import com.programmers.vouchermanagement.customer.service.CustomerService; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Controller; + +import java.util.List; + +@Profile("console") +@Controller +public class CustomerConsoleController { + private final CustomerService customerService; + + public CustomerConsoleController(CustomerService customerService) { + this.customerService = customerService; + } + + public CustomerResponse create(CreateCustomerRequest createCustomerRequest) { + return customerService.create(createCustomerRequest); + } + + public List readAll() { + return customerService.readAll(); + } + + public List readAllBlackCustomer() { + return customerService.readAllBlackCustomer(); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/customer/controller/CustomerRestController.java b/src/main/java/com/programmers/vouchermanagement/customer/controller/CustomerRestController.java new file mode 100644 index 0000000000..c907b782fe --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/controller/CustomerRestController.java @@ -0,0 +1,44 @@ +package com.programmers.vouchermanagement.customer.controller; + +import com.programmers.vouchermanagement.customer.controller.dto.CreateCustomerRequest; +import com.programmers.vouchermanagement.customer.controller.dto.CustomerResponse; +import com.programmers.vouchermanagement.customer.service.CustomerService; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@Profile("api") +@RequestMapping("/api/v1/customers") +@RestController +public class CustomerRestController { + private final CustomerService customerService; + + public CustomerRestController(CustomerService customerService) { + this.customerService = customerService; + } + + @PostMapping + public ResponseEntity create( + @RequestBody + CreateCustomerRequest createCustomerRequest + ) { + return ResponseEntity.status(HttpStatus.CREATED) + .body(customerService.create(createCustomerRequest)); + } + + @GetMapping + public ResponseEntity> readAll( + @RequestParam(name = "type", defaultValue = "all") + String type + ) { + if (type.equals("blacklist")) + return ResponseEntity.status(HttpStatus.OK) + .body(customerService.readAllBlackCustomer()); + + return ResponseEntity.status(HttpStatus.OK) + .body(customerService.readAll()); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/customer/controller/CustomerThymeleafController.java b/src/main/java/com/programmers/vouchermanagement/customer/controller/CustomerThymeleafController.java new file mode 100644 index 0000000000..56acc31182 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/controller/CustomerThymeleafController.java @@ -0,0 +1,45 @@ +package com.programmers.vouchermanagement.customer.controller; + +import com.programmers.vouchermanagement.customer.controller.dto.CreateCustomerRequest; +import com.programmers.vouchermanagement.customer.service.CustomerService; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Profile("thyme") +@RequestMapping("/customers") +@Controller +public class CustomerThymeleafController { + private final CustomerService customerService; + + public CustomerThymeleafController(CustomerService customerService) { + this.customerService = customerService; + } + + @PostMapping("/new") + public String create(CreateCustomerRequest createCustomerRequest) { + customerService.create(createCustomerRequest); + return "redirect:/customers"; + } + + @GetMapping("/new") + public String viewCreatePage() { + return "customer/customer-new"; + } + + @GetMapping + public String readAll(@RequestParam(required = false, defaultValue = "all") String type, Model model) { + if (type.equals("blacklist")) { + model.addAttribute("mode", "blacklist"); + model.addAttribute("customers", customerService.readAllBlackCustomer()); + return "customer/customers"; + } + model.addAttribute("mode", "all"); + model.addAttribute("customers", customerService.readAll()); + return "customer/customers"; + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/customer/controller/dto/CreateCustomerRequest.java b/src/main/java/com/programmers/vouchermanagement/customer/controller/dto/CreateCustomerRequest.java new file mode 100644 index 0000000000..503104308c --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/controller/dto/CreateCustomerRequest.java @@ -0,0 +1,4 @@ +package com.programmers.vouchermanagement.customer.controller.dto; + +public record CreateCustomerRequest(String name, boolean isBlack) { +} \ No newline at end of file diff --git a/src/main/java/com/programmers/vouchermanagement/customer/controller/dto/CustomerResponse.java b/src/main/java/com/programmers/vouchermanagement/customer/controller/dto/CustomerResponse.java new file mode 100644 index 0000000000..506ce1f6cc --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/controller/dto/CustomerResponse.java @@ -0,0 +1,11 @@ +package com.programmers.vouchermanagement.customer.controller.dto; + +import com.programmers.vouchermanagement.customer.domain.Customer; + +import java.util.UUID; + +public record CustomerResponse(UUID id, String name, boolean isBlack) { + public static CustomerResponse from(Customer customer) { + return new CustomerResponse(customer.getId(), customer.getName(), customer.getIsBlack()); + } +} \ No newline at end of file diff --git a/src/main/java/com/programmers/vouchermanagement/customer/domain/Customer.java b/src/main/java/com/programmers/vouchermanagement/customer/domain/Customer.java new file mode 100644 index 0000000000..996f05336a --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/domain/Customer.java @@ -0,0 +1,52 @@ +package com.programmers.vouchermanagement.customer.domain; + +import java.util.Objects; +import java.util.UUID; + +public class Customer { + private final UUID id; + private final String name; + private final boolean isBlack; + + public Customer(UUID id, String name, boolean isBlack) { + validateName(name); + this.id = id; + this.name = name; + this.isBlack = isBlack; + } + + public Customer(String name, boolean isBlack) { + this(UUID.randomUUID(), name, isBlack); + } + + private void validateName(String name) { + if (name == null || name.isBlank() || name.length() > 20) + throw new IllegalArgumentException("The name length should be between 0 to 20."); + } + + public UUID getId() { + return id; + } + + public boolean getIsBlack() { + return isBlack; + } + + public String getName() { + return name; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Customer customer = (Customer) o; + return isBlack == customer.isBlack && Objects.equals(id, customer.id) && Objects.equals(name, customer.name); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, isBlack); + } +} + diff --git a/src/main/java/com/programmers/vouchermanagement/customer/repository/CustomerFileRepository.java b/src/main/java/com/programmers/vouchermanagement/customer/repository/CustomerFileRepository.java new file mode 100644 index 0000000000..05c7340a02 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/repository/CustomerFileRepository.java @@ -0,0 +1,41 @@ +package com.programmers.vouchermanagement.customer.repository; + +import com.programmers.vouchermanagement.customer.domain.Customer; +import com.programmers.vouchermanagement.customer.repository.util.CustomerFileManager; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Repository +@Profile({"file", "memory"}) +public class CustomerFileRepository implements CustomerRepository { + public final Map customers; + private final CustomerFileManager customerFileManager; + + public CustomerFileRepository(CustomerFileManager customerFileManager) { + this.customerFileManager = customerFileManager; + customers = customerFileManager.loadFile(); + } + + @Override + public void insert(Customer customer) { + customers.put(customer.getId(), customer); + customerFileManager.saveFile(customers); + } + + @Override + public List findAll() { + return customers.values().stream().toList(); + } + + @Override + public List findAllBlackCustomer() { + return customers.values() + .stream() + .filter(Customer::getIsBlack) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/programmers/vouchermanagement/customer/repository/CustomerJDBCRepository.java b/src/main/java/com/programmers/vouchermanagement/customer/repository/CustomerJDBCRepository.java new file mode 100644 index 0000000000..881dbe7254 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/repository/CustomerJDBCRepository.java @@ -0,0 +1,53 @@ +package com.programmers.vouchermanagement.customer.repository; + +import com.programmers.vouchermanagement.customer.domain.Customer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.List; + +import static com.programmers.vouchermanagement.customer.repository.util.CustomerDomainMapper.customerRowMapper; +import static com.programmers.vouchermanagement.customer.repository.util.CustomerDomainMapper.customerToParamMap; +import static com.programmers.vouchermanagement.util.Constant.UPDATE_ONE_FLAG; +import static com.programmers.vouchermanagement.util.Message.NOT_INSERTED; + +@Repository +@Profile("jdbc") +public class CustomerJDBCRepository implements CustomerRepository { + private static final Logger logger = LoggerFactory.getLogger(CustomerJDBCRepository.class); + private final NamedParameterJdbcTemplate jdbcTemplate; + + public CustomerJDBCRepository(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void insert(Customer customer) { + int update = jdbcTemplate.update( + "INSERT INTO customers(id, name, black) VALUES (UUID_TO_BIN(:id), :name, :black)", + customerToParamMap(customer)); + if (update != UPDATE_ONE_FLAG) { + logger.error(NOT_INSERTED); + throw new EmptyResultDataAccessException(UPDATE_ONE_FLAG); + } + } + + @Override + public List findAllBlackCustomer() { + return jdbcTemplate.query( + "SELECT * FROM customers WHERE black = TRUE", + customerRowMapper); + } + + + @Override + public List findAll() { + return jdbcTemplate.query( + "SELECT * FROM customers", + customerRowMapper); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/customer/repository/CustomerRepository.java b/src/main/java/com/programmers/vouchermanagement/customer/repository/CustomerRepository.java new file mode 100644 index 0000000000..5ee7d00dc9 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/repository/CustomerRepository.java @@ -0,0 +1,13 @@ +package com.programmers.vouchermanagement.customer.repository; + +import com.programmers.vouchermanagement.customer.domain.Customer; + +import java.util.List; + +public interface CustomerRepository { + void insert(Customer customer); + + List findAll(); + + List findAllBlackCustomer(); +} diff --git a/src/main/java/com/programmers/vouchermanagement/customer/repository/util/CustomerDomainMapper.java b/src/main/java/com/programmers/vouchermanagement/customer/repository/util/CustomerDomainMapper.java new file mode 100644 index 0000000000..4a72f6ceb8 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/repository/util/CustomerDomainMapper.java @@ -0,0 +1,48 @@ +package com.programmers.vouchermanagement.customer.repository.util; + +import com.programmers.vouchermanagement.customer.domain.Customer; +import com.programmers.vouchermanagement.util.DomainMapper; +import org.springframework.jdbc.core.RowMapper; + +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static com.programmers.vouchermanagement.util.Constant.COMMA_SEPARATOR; + +public class CustomerDomainMapper extends DomainMapper { + public static final String BLACK_KEY = "black"; + public static final String NAME_KEY = "name"; + public static final RowMapper customerRowMapper = (resultSet, i) -> { + UUID id = toUUID(resultSet.getBytes(ID_KEY)); + String name = resultSet.getString(NAME_KEY); + boolean isBlack = resultSet.getBoolean(BLACK_KEY); + + return new Customer(id, name, isBlack); + }; + + private CustomerDomainMapper() { + } + + public static Map customerToParamMap(Customer customer) { + Map paramMap = new HashMap<>(); + paramMap.put(ID_KEY, customer.getId().toString().getBytes()); + paramMap.put(NAME_KEY, customer.getName()); + paramMap.put(BLACK_KEY, customer.getIsBlack()); + return paramMap; + } + + public static Customer stringToCustomer(String line) { + String[] customerInfo = line.split(COMMA_SEPARATOR); + UUID customerId = UUID.fromString(customerInfo[0]); + String name = customerInfo[1]; + boolean isBlack = Boolean.parseBoolean(customerInfo[2]); + return new Customer(customerId, name, isBlack); + } + + public static String customerToString(Customer customer) { + return customer.getId().toString() + COMMA_SEPARATOR + + customer.getName() + COMMA_SEPARATOR + + customer.getIsBlack(); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/customer/repository/util/CustomerFileManager.java b/src/main/java/com/programmers/vouchermanagement/customer/repository/util/CustomerFileManager.java new file mode 100644 index 0000000000..f41bdff32a --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/repository/util/CustomerFileManager.java @@ -0,0 +1,61 @@ +package com.programmers.vouchermanagement.customer.repository.util; + +import com.programmers.vouchermanagement.customer.domain.Customer; +import com.programmers.vouchermanagement.properties.AppProperties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.io.*; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +import static com.programmers.vouchermanagement.customer.repository.util.CustomerDomainMapper.customerToString; +import static com.programmers.vouchermanagement.customer.repository.util.CustomerDomainMapper.stringToCustomer; +import static com.programmers.vouchermanagement.util.Message.FILE_EXCEPTION; +import static com.programmers.vouchermanagement.util.Message.IO_EXCEPTION; + +@Component +@Profile("file") +public class CustomerFileManager { + private static final Logger logger = LoggerFactory.getLogger(CustomerFileManager.class); + private final String filePath; + + public CustomerFileManager(AppProperties appProperties) { + this.filePath = appProperties.resources().path() + appProperties.domains().get("customer").fileName(); + } + + public Map loadFile() { + try (BufferedReader br = new BufferedReader(new FileReader(filePath))) { + br.readLine(); // skip the first line + return loadCustomer(br); + } catch (IOException e) { + logger.warn(IO_EXCEPTION); + throw new UncheckedIOException(e); + } + } + + private Map loadCustomer(BufferedReader br) throws IOException { + Map customers = new HashMap<>(); + String str; + while ((str = br.readLine()) != null) { + Customer customer = stringToCustomer(str); + customers.put(customer.getId(), customer); + } + return customers; + } + + + public void saveFile(Map customers) { + File csvOutputFile = new File(filePath); + try (PrintWriter pw = new PrintWriter(csvOutputFile)) { + customers.values().forEach((customer) -> pw.println(customerToString(customer))); + } catch (FileNotFoundException e) { + logger.warn(FILE_EXCEPTION); + throw new RuntimeException(e); + } + } + +} diff --git a/src/main/java/com/programmers/vouchermanagement/customer/service/CustomerService.java b/src/main/java/com/programmers/vouchermanagement/customer/service/CustomerService.java new file mode 100644 index 0000000000..810cb1fee2 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/customer/service/CustomerService.java @@ -0,0 +1,43 @@ +package com.programmers.vouchermanagement.customer.service; + +import com.programmers.vouchermanagement.customer.controller.dto.CreateCustomerRequest; +import com.programmers.vouchermanagement.customer.controller.dto.CustomerResponse; +import com.programmers.vouchermanagement.customer.domain.Customer; +import com.programmers.vouchermanagement.customer.repository.CustomerRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collections; +import java.util.List; + +@Service +public class CustomerService { + private final CustomerRepository customerRepository; + + public CustomerService(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + @Transactional + public CustomerResponse create(CreateCustomerRequest createCustomerRequest) { + Customer customer = new Customer(createCustomerRequest.name(), createCustomerRequest.isBlack()); + customerRepository.insert(customer); + return CustomerResponse.from(customer); + } + + @Transactional(readOnly = true) + public List readAll() { + List customers = customerRepository.findAll(); + + return customers.stream().map(CustomerResponse::from).toList(); + } + + @Transactional(readOnly = true) + public List readAllBlackCustomer() { + List blacklist = customerRepository.findAllBlackCustomer(); + if (blacklist.isEmpty()) { + return Collections.emptyList(); + } + return blacklist.stream().map(CustomerResponse::from).toList(); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/properties/AppProperties.java b/src/main/java/com/programmers/vouchermanagement/properties/AppProperties.java new file mode 100644 index 0000000000..edd42aa8dc --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/properties/AppProperties.java @@ -0,0 +1,9 @@ +package com.programmers.vouchermanagement.properties; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.Map; + +@ConfigurationProperties(prefix = "file") +public record AppProperties(Resources resources, Map domains) { +} diff --git a/src/main/java/com/programmers/vouchermanagement/properties/Domain.java b/src/main/java/com/programmers/vouchermanagement/properties/Domain.java new file mode 100644 index 0000000000..e0c6981776 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/properties/Domain.java @@ -0,0 +1,4 @@ +package com.programmers.vouchermanagement.properties; + +public record Domain(String fileName) { +} diff --git a/src/main/java/com/programmers/vouchermanagement/properties/Resources.java b/src/main/java/com/programmers/vouchermanagement/properties/Resources.java new file mode 100644 index 0000000000..4643c891c0 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/properties/Resources.java @@ -0,0 +1,4 @@ +package com.programmers.vouchermanagement.properties; + +public record Resources(String path, String buildPath) { +} diff --git a/src/main/java/com/programmers/vouchermanagement/util/Constant.java b/src/main/java/com/programmers/vouchermanagement/util/Constant.java new file mode 100644 index 0000000000..c596c1aef5 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/util/Constant.java @@ -0,0 +1,10 @@ +package com.programmers.vouchermanagement.util; + +public class Constant { + public static final String LINE_SEPARATOR = System.lineSeparator(); + public static final String COMMA_SEPARATOR = ", "; + public static final int UPDATE_ONE_FLAG = 1; + public static final int UPDATE_ZERO_FLAG = 0; + private Constant() { + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/util/DomainMapper.java b/src/main/java/com/programmers/vouchermanagement/util/DomainMapper.java new file mode 100644 index 0000000000..fd9eecb7f9 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/util/DomainMapper.java @@ -0,0 +1,24 @@ +package com.programmers.vouchermanagement.util; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class DomainMapper { + public static final String ID_KEY = "id"; + + protected DomainMapper() { + } + + protected static UUID toUUID(byte[] bytes) { + var byteBuffer = ByteBuffer.wrap(bytes); + return new UUID(byteBuffer.getLong(), byteBuffer.getLong()); + } + + public static Map uuidToParamMap(UUID id) { + Map paramMap = new HashMap<>(); + paramMap.put(ID_KEY, id.toString().getBytes()); + return paramMap; + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/util/Message.java b/src/main/java/com/programmers/vouchermanagement/util/Message.java new file mode 100644 index 0000000000..45393e6935 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/util/Message.java @@ -0,0 +1,20 @@ +package com.programmers.vouchermanagement.util; + +public class Message { + public static final String NOT_INSERTED = "Noting was inserted!"; + public static final String NOT_UPDATED = "Noting was updated!"; + public static final String NOT_DELETED = "Noting was deleted!"; + public static final String EMPTY_RESULT = "Got empty result!"; + public static final String IO_EXCEPTION = "Error raised while reading file!"; + public static final String NOT_FOUND_VOUCHER = "There is no such voucher."; + public static final String NOT_FOUND_CUSTOMER = "There is no such customer."; + public static final String NOT_FOUND_VOUCHER_ALLOCATION = "There is no voucher allocation information."; + public static final String NOT_FOUND_CUSTOMER_ALLOCATION = "There is no customer allocation information."; + public static final String CAN_NOT_INSERT_OWNERSHIP = "There is no customer allocation information."; + public static final String ALREADY_EMPTY_TABLE = "The table is already empty."; + public static final String FILE_EXCEPTION = "Error raised while opening the file."; + public static final String INVALID_VOUCHER_TYPE = "Voucher type should be either fixed amount or percent discount voucher."; + + private Message() { + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/controller/VoucherConsoleController.java b/src/main/java/com/programmers/vouchermanagement/voucher/controller/VoucherConsoleController.java new file mode 100644 index 0000000000..78b12e73ec --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/controller/VoucherConsoleController.java @@ -0,0 +1,27 @@ +package com.programmers.vouchermanagement.voucher.controller; + +import com.programmers.vouchermanagement.voucher.controller.dto.CreateVoucherRequest; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import com.programmers.vouchermanagement.voucher.service.VoucherService; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Controller; + +import java.util.List; + +@Profile("console") +@Controller +public class VoucherConsoleController { + private final VoucherService voucherService; + + public VoucherConsoleController(VoucherService voucherService) { + this.voucherService = voucherService; + } + + public VoucherResponse create(CreateVoucherRequest createVoucherRequest) { + return voucherService.create(createVoucherRequest); + } + + public List readAll() { + return voucherService.readAll(); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/controller/VoucherRestController.java b/src/main/java/com/programmers/vouchermanagement/voucher/controller/VoucherRestController.java new file mode 100644 index 0000000000..c49872ed4c --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/controller/VoucherRestController.java @@ -0,0 +1,105 @@ +package com.programmers.vouchermanagement.voucher.controller; + +import com.programmers.vouchermanagement.voucher.controller.dto.CreateVoucherRequest; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import com.programmers.vouchermanagement.voucher.service.VoucherService; +import org.springframework.context.annotation.Profile; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.net.URI; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +@Profile("api") +@RequestMapping("/api/v1/vouchers") +@RestController +public class VoucherRestController { + private final VoucherService voucherService; + + public VoucherRestController(VoucherService voucherService) { + this.voucherService = voucherService; + } + + @PostMapping + public ResponseEntity create( + @RequestBody + CreateVoucherRequest createVoucherRequest + ) { + VoucherResponse voucherResponse = voucherService.create(createVoucherRequest); + return ResponseEntity.created(URI.create("/api/v1/vouchers/" + voucherResponse.id())) + .body(voucherResponse); + } + + @GetMapping("/{voucherId}") + public ResponseEntity readById( + @PathVariable("voucherId") + UUID voucherId + ) { + return ResponseEntity.status(HttpStatus.OK) + .body(voucherService.readById(voucherId)); + } + + @DeleteMapping("/{voucherId}") + public ResponseEntity delete( + @PathVariable("voucherId") + UUID voucherId + ) { + return ResponseEntity.status(HttpStatus.OK) + .body(voucherService.delete(voucherId)); + } + + @PutMapping("/{voucherId}") + public ResponseEntity update( + @PathVariable("voucherId") + UUID voucherId, + @RequestBody + CreateVoucherRequest createVoucherRequest + ) { + return ResponseEntity.status(HttpStatus.OK) + .body(voucherService.update(voucherId, createVoucherRequest)); + } + + @GetMapping + public ResponseEntity> readAll( + //TODO: make Object and Validate! + @RequestParam(name = "filter", defaultValue = "all") + String filter, + @RequestParam(name = "from", required = false) + LocalDate from, + @RequestParam(name = "to", required = false) + LocalDate to, + @RequestParam(name = "type-name", required = false) + String typeName + ) { + if (filter.equals("all")) + return ResponseEntity.status(HttpStatus.OK) + .body(voucherService.readAll()); + + if (filter.equals("created-at") && from != null) { //TODO: validate date + LocalTime localTime = LocalTime.of(0, 0, 0); + LocalDateTime fromDateTime = LocalDateTime.of( + from, + localTime); + + LocalDateTime toDateTime = LocalDateTime.of( + to == null ? LocalDate.now() : to.plusDays(1), + localTime); + + return ResponseEntity.status(HttpStatus.OK) + .body(voucherService.readAllByCreatedAt(fromDateTime, toDateTime)); + } + + if (filter.equals("type") && typeName != null) + return ResponseEntity.status(HttpStatus.OK) + .body(voucherService.readAllByType(typeName)); + + return ResponseEntity.status(HttpStatus.BAD_REQUEST) + .body(Collections.emptyList()); //TODO: apply ControllerAdvice + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/controller/VoucherThymeleafController.java b/src/main/java/com/programmers/vouchermanagement/voucher/controller/VoucherThymeleafController.java new file mode 100644 index 0000000000..c091707711 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/controller/VoucherThymeleafController.java @@ -0,0 +1,76 @@ +package com.programmers.vouchermanagement.voucher.controller; + +import com.programmers.vouchermanagement.voucher.controller.dto.CreateVoucherRequest; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import com.programmers.vouchermanagement.voucher.service.VoucherService; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@Profile("thyme") +@RequestMapping("/vouchers") +@Controller +public class VoucherThymeleafController { + private final VoucherService voucherService; + + public VoucherThymeleafController(VoucherService voucherService) { + this.voucherService = voucherService; + } + + @PostMapping("/new") + public String create(CreateVoucherRequest createVoucherRequest) { + voucherService.create(createVoucherRequest); + return "redirect:/vouchers"; + } + + @GetMapping("/new") + public String ViewCreatePage(Model model) { + return "voucher/voucher-new"; + } + + @GetMapping + public String viewVouchersPage(Model model) { + List vouchers = voucherService.readAll(); + model.addAttribute("vouchers", vouchers); + return "voucher/vouchers"; + } + + @GetMapping("/{voucherId}") + public String readById(@PathVariable("voucherId") UUID voucherId, Model model) { + try { + VoucherResponse voucher = voucherService.readById(voucherId); + model.addAttribute("voucher", voucher); + return "voucher/voucher-detail"; + } catch (RuntimeException e) { + return "views/404"; + } + } + + @DeleteMapping("/{voucherId}") + public String delete(@PathVariable("voucherId") UUID voucherId) { + voucherService.delete(voucherId); + return "redirect:/vouchers"; + } + + @PutMapping("/update/{voucherId}") + public String update(@PathVariable("voucherId") UUID voucherId, CreateVoucherRequest createVoucherRequest) { + voucherService.update(voucherId, createVoucherRequest); + return "redirect:/vouchers"; + } + //TODO: check id + + @GetMapping("/update/{voucherId}") + public String viewUpdatePage(@PathVariable("voucherId") UUID voucherId, Model model) { + try { + VoucherResponse voucher = voucherService.readById(voucherId); + model.addAttribute("voucher", voucher); + return "voucher/voucher-update"; + } catch (RuntimeException e) { + return "views/404"; + } + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/controller/dto/CreateVoucherRequest.java b/src/main/java/com/programmers/vouchermanagement/voucher/controller/dto/CreateVoucherRequest.java new file mode 100644 index 0000000000..d48f874042 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/controller/dto/CreateVoucherRequest.java @@ -0,0 +1,4 @@ +package com.programmers.vouchermanagement.voucher.controller.dto; + +public record CreateVoucherRequest(String typeName, long discountValue) { +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/controller/dto/VoucherResponse.java b/src/main/java/com/programmers/vouchermanagement/voucher/controller/dto/VoucherResponse.java new file mode 100644 index 0000000000..82268ce86b --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/controller/dto/VoucherResponse.java @@ -0,0 +1,12 @@ +package com.programmers.vouchermanagement.voucher.controller.dto; + +import com.programmers.vouchermanagement.voucher.domain.Voucher; + +import java.time.LocalDateTime; +import java.util.UUID; + +public record VoucherResponse(UUID id, LocalDateTime createdAt, String typeName, long discountValue) { + public static VoucherResponse from(Voucher voucher) { + return new VoucherResponse(voucher.getId(), voucher.getCreatedAt(), voucher.getTypeName(), voucher.getDiscountValue()); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/domain/Voucher.java b/src/main/java/com/programmers/vouchermanagement/voucher/domain/Voucher.java new file mode 100644 index 0000000000..2959874626 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/domain/Voucher.java @@ -0,0 +1,57 @@ +package com.programmers.vouchermanagement.voucher.domain; + +import com.programmers.vouchermanagement.voucher.domain.vouchertype.VoucherType; +import com.programmers.vouchermanagement.voucher.domain.vouchertype.VoucherTypeManager; + +import java.time.LocalDateTime; +import java.util.Objects; +import java.util.UUID; + +public class Voucher { + private final UUID id; + private final LocalDateTime createdAt; + private VoucherType type; + private long discountValue; + + public Voucher(UUID id, LocalDateTime createdAt, String typeName, long discountValue) { + this.id = id; + this.createdAt = createdAt; + this.type = VoucherTypeManager.get(typeName); + type.validateDiscountValue(discountValue); + this.discountValue = discountValue; + } + + public Voucher(String typeName, long discountValue) { + this(UUID.randomUUID(), LocalDateTime.now(), typeName, discountValue); + } + + + public String getTypeName() { + return type.getName(); + } + + public UUID getId() { + return id; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public long getDiscountValue() { + return discountValue; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Voucher voucher = (Voucher) o; + return discountValue == voucher.discountValue && Objects.equals(id, voucher.id) && Objects.equals(createdAt, voucher.createdAt) && Objects.equals(type, voucher.type); + } + + @Override + public int hashCode() { + return Objects.hash(id, createdAt, type, discountValue); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/FixedAmountVoucherType.java b/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/FixedAmountVoucherType.java new file mode 100644 index 0000000000..a05edaa634 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/FixedAmountVoucherType.java @@ -0,0 +1,28 @@ +package com.programmers.vouchermanagement.voucher.domain.vouchertype; + +import java.text.MessageFormat; + +public class FixedAmountVoucherType extends VoucherType { + private static final String NAME = "FIXED"; + private static final FixedAmountVoucherType INSTANCE = new FixedAmountVoucherType(); + private static final long MAX_DISCOUNT_VALUE = 100000000; + + private FixedAmountVoucherType() { + } + + public static VoucherType getInstance() { + return INSTANCE; + } + + @Override + public void validateDiscountValue(long discountValue) { + if (discountValue < MIN_DISCOUNT_VALUE || MAX_DISCOUNT_VALUE < discountValue) { + throw new IllegalArgumentException(MessageFormat.format("The discount price({0}) is not appropriate at FixedAmountVoucher.", discountValue)); + } + } + + @Override + public String getName() { + return NAME; + } +} \ No newline at end of file diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/PercentVoucherType.java b/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/PercentVoucherType.java new file mode 100644 index 0000000000..5ce616308d --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/PercentVoucherType.java @@ -0,0 +1,28 @@ +package com.programmers.vouchermanagement.voucher.domain.vouchertype; + +import java.text.MessageFormat; + +public class PercentVoucherType extends VoucherType { + private static final String NAME = "PERCENT"; + private static final PercentVoucherType INSTANCE = new PercentVoucherType(); + private static final long MAX_DISCOUNT_VALUE = 100; + + private PercentVoucherType() { + } + + public static VoucherType getInstance() { + return INSTANCE; + } + + @Override + public void validateDiscountValue(long discountValue) { + if (discountValue < MIN_DISCOUNT_VALUE || MAX_DISCOUNT_VALUE < discountValue) { + throw new IllegalArgumentException(MessageFormat.format("The discount price({0}) is not appropriate at PercentVoucher.", discountValue)); + } + } + + @Override + public String getName() { + return NAME; + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/VoucherType.java b/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/VoucherType.java new file mode 100644 index 0000000000..aa23bc4d71 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/VoucherType.java @@ -0,0 +1,9 @@ +package com.programmers.vouchermanagement.voucher.domain.vouchertype; + +public abstract class VoucherType { + protected static final long MIN_DISCOUNT_VALUE = 0; + + abstract public void validateDiscountValue(long discountValue); + + abstract public String getName(); +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/VoucherTypeManager.java b/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/VoucherTypeManager.java new file mode 100644 index 0000000000..8036ab501b --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/VoucherTypeManager.java @@ -0,0 +1,27 @@ +package com.programmers.vouchermanagement.voucher.domain.vouchertype; + +import java.text.MessageFormat; +import java.util.Arrays; +import java.util.function.Supplier; + +public enum VoucherTypeManager { + FIXED(FixedAmountVoucherType::getInstance), + PERCENT(PercentVoucherType::getInstance); + + private final Supplier voucherTypeSupplier; + + VoucherTypeManager(Supplier voucherTypeSupplier) { + this.voucherTypeSupplier = voucherTypeSupplier; + } + + public static VoucherType get(String typeName) { + return getGenerator(typeName).voucherTypeSupplier.get(); + } + + private static VoucherTypeManager getGenerator(String typeName) { + return Arrays.stream(values()) + .filter(type -> type.name().equals(typeName)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(MessageFormat.format("There is no such type: {0}.", typeName))); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherFileRepository.java b/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherFileRepository.java new file mode 100644 index 0000000000..3c71cd38bc --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherFileRepository.java @@ -0,0 +1,85 @@ +package com.programmers.vouchermanagement.voucher.repository; + +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.voucher.domain.vouchertype.VoucherType; +import com.programmers.vouchermanagement.voucher.repository.util.VoucherFileManager; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.*; + +import static com.programmers.vouchermanagement.util.Message.NOT_DELETED; +import static com.programmers.vouchermanagement.util.Message.NOT_UPDATED; + +@Repository +@Profile("file") +public class VoucherFileRepository implements VoucherRepository { + public final Map vouchers; + private final VoucherFileManager voucherFileManager; + + public VoucherFileRepository(VoucherFileManager voucherFileManager) { + this.voucherFileManager = voucherFileManager; + vouchers = voucherFileManager.loadFile(); + } + + @Override + public void insert(Voucher voucher) { + vouchers.put(voucher.getId(), voucher); + voucherFileManager.saveFile(vouchers); + } + + @Override + public List findAll() { + return vouchers.values().stream().toList(); + } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(vouchers.get(id)); + } + + @Override + public List findAllByCreatedAt(LocalDateTime from, LocalDateTime to) { + return vouchers.values() + .stream() + .filter(voucher -> { + LocalDateTime createdAt = voucher.getCreatedAt(); + return createdAt.isAfter(from) && createdAt.isBefore(to); + }) + .toList(); + } + + @Override + public void delete(UUID id) { + Optional.ofNullable(vouchers.remove(id)) + .orElseThrow(() -> new NoSuchElementException(NOT_DELETED)); + voucherFileManager.saveFile(vouchers); + } + + @Override + public void deleteAll() { + if (!vouchers.isEmpty()) vouchers.clear(); + voucherFileManager.saveFile(vouchers); + } + + @Override + public void update(Voucher voucher) { + Optional.ofNullable(vouchers.get(voucher.getId())) + .orElseThrow(() -> new NoSuchElementException(NOT_UPDATED)); + vouchers.put(voucher.getId(), voucher); + voucherFileManager.saveFile(vouchers); + } + + @Override + public List findAllByType(VoucherType voucherType) { + return vouchers.values() + .stream() + .filter(voucher -> isEqualName(voucher, voucherType)) + .toList(); + } + + private boolean isEqualName(Voucher voucher, VoucherType voucherType) { + return voucher.getTypeName().equals(voucherType.getName()); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherInMemoryRepository.java b/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherInMemoryRepository.java new file mode 100644 index 0000000000..f23d842d4b --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherInMemoryRepository.java @@ -0,0 +1,68 @@ +package com.programmers.vouchermanagement.voucher.repository; + +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.voucher.domain.vouchertype.VoucherType; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.*; + +import static com.programmers.vouchermanagement.util.Message.NOT_DELETED; +import static com.programmers.vouchermanagement.util.Message.NOT_UPDATED; + +@Repository +@Profile("memory") +public class VoucherInMemoryRepository implements VoucherRepository { + private final Map vouchers; + + public VoucherInMemoryRepository() { + vouchers = new HashMap<>(); + } + + @Override + public void insert(Voucher voucher) { + vouchers.put(voucher.getId(), voucher); + } + + @Override + public List findAll() { + return vouchers.values().stream().toList(); + } + + @Override + public Optional findById(UUID id) { + return Optional.ofNullable(vouchers.get(id)); + } + + @Override + public List findAllByCreatedAt(LocalDateTime from, LocalDateTime to) { + return vouchers.values().stream().filter(voucher -> isCreatedBetweenPeriod(voucher, from, to)).toList(); + } + + private boolean isCreatedBetweenPeriod(Voucher voucher, LocalDateTime from, LocalDateTime to) { + LocalDateTime createdAt = voucher.getCreatedAt(); + return createdAt.isAfter(from) && createdAt.isBefore(to); + } + + @Override + public void delete(UUID id) { + Optional.ofNullable(vouchers.remove(id)).orElseThrow(() -> new NoSuchElementException(NOT_DELETED)); + } + + @Override + public void deleteAll() { + if (!vouchers.isEmpty()) vouchers.clear(); + } + + @Override + public void update(Voucher voucher) { + Optional.ofNullable(vouchers.get(voucher.getId())).orElseThrow(() -> new NoSuchElementException(NOT_UPDATED)); + vouchers.put(voucher.getId(), voucher); + } + + @Override + public List findAllByType(VoucherType voucherType) { + return vouchers.values().stream().filter(voucher -> voucher.getTypeName().equals(voucherType.getName())).toList(); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherJDBCRepository.java b/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherJDBCRepository.java new file mode 100644 index 0000000000..d6fdec1045 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherJDBCRepository.java @@ -0,0 +1,98 @@ +package com.programmers.vouchermanagement.voucher.repository; + +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.voucher.domain.vouchertype.VoucherType; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.*; + +import static com.programmers.vouchermanagement.util.Constant.UPDATE_ONE_FLAG; +import static com.programmers.vouchermanagement.util.Message.*; +import static com.programmers.vouchermanagement.voucher.repository.util.VoucherDomainMapper.*; + +@Profile("jdbc") +@Repository +public class VoucherJDBCRepository implements VoucherRepository { + private static final Logger logger = LoggerFactory.getLogger(VoucherJDBCRepository.class); + private final NamedParameterJdbcTemplate jdbcTemplate; + + public VoucherJDBCRepository(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void insert(Voucher voucher) { + int update = jdbcTemplate.update( + "INSERT INTO vouchers(id, type, discount_value) VALUES (UUID_TO_BIN(:id), :type, :discount_value)", + voucherToParamMap(voucher)); + if (update != UPDATE_ONE_FLAG) { + throw new EmptyResultDataAccessException(UPDATE_ONE_FLAG); + } + } + + @Override + public List findAll() { + return jdbcTemplate.query( + "SELECT * FROM vouchers", + voucherRowMapper); + } + + @Override + public Optional findById(UUID id) { + try { + return Optional.ofNullable(jdbcTemplate.queryForObject( + "SELECT * FROM vouchers WHERE id = UUID_TO_BIN(:id)", + Collections.singletonMap(ID_KEY, id.toString().getBytes()), voucherRowMapper)); + } catch (EmptyResultDataAccessException e) { + logger.error(EMPTY_RESULT, e); + return Optional.empty(); + } + } + + @Override + public List findAllByCreatedAt(LocalDateTime from, LocalDateTime to) { + return jdbcTemplate.query( + "SELECT * FROM vouchers WHERE created_at BETWEEN DATE(:from) AND DATE(:to)", + Map.of(FROM_KEY, from.toString(), TO_KEY, to.toString()), voucherRowMapper); + } + + @Override + public void delete(UUID id) { + int update = jdbcTemplate.update( + "DELETE FROM vouchers WHERE id = UUID_TO_BIN(:id)", + Collections.singletonMap(ID_KEY, id.toString().getBytes())); + if (update != UPDATE_ONE_FLAG) { + throw new NoSuchElementException(NOT_DELETED); + } + } + + @Override + public void deleteAll() { + jdbcTemplate.update( + "DELETE FROM vouchers", + Collections.emptyMap()); + } + + @Override + public void update(Voucher voucher) { + int update = jdbcTemplate.update( + "UPDATE vouchers SET type = :type, discount_value = :discount_value WHERE id = UUID_TO_BIN(:id)", + voucherToParamMap(voucher)); + if (update != UPDATE_ONE_FLAG) { + throw new NoSuchElementException(NOT_UPDATED); + } + } + + @Override + public List findAllByType(VoucherType type) { + return jdbcTemplate.query( + "SELECT * FROM vouchers WHERE type = :type", + Map.of(TYPE_KEY, type.getName()), voucherRowMapper); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherRepository.java b/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherRepository.java new file mode 100644 index 0000000000..b60550a253 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/repository/VoucherRepository.java @@ -0,0 +1,27 @@ +package com.programmers.vouchermanagement.voucher.repository; + +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.voucher.domain.vouchertype.VoucherType; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface VoucherRepository { + void insert(Voucher voucher); + + List findAll(); + + Optional findById(UUID id); + + List findAllByCreatedAt(LocalDateTime from, LocalDateTime to); + + void delete(UUID id); + + void deleteAll(); + + void update(Voucher voucher); + + List findAllByType(VoucherType voucherType); +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/repository/util/VoucherDomainMapper.java b/src/main/java/com/programmers/vouchermanagement/voucher/repository/util/VoucherDomainMapper.java new file mode 100644 index 0000000000..4e9b74b0f6 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/repository/util/VoucherDomainMapper.java @@ -0,0 +1,58 @@ +package com.programmers.vouchermanagement.voucher.repository.util; + +import com.programmers.vouchermanagement.util.DomainMapper; +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import org.springframework.jdbc.core.RowMapper; + +import java.sql.Timestamp; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +public class VoucherDomainMapper extends DomainMapper { + public static final String DISCOUNT_VALUE_KEY = "discount_value"; + public static final String TYPE_KEY = "type"; + public static final String CREATED_AT_KEY = "created_at"; + public static final String FROM_KEY = "from"; + public static final String TO_KEY = "to"; + public static final RowMapper voucherRowMapper = (resultSet, i) -> { + UUID id = toUUID(resultSet.getBytes(ID_KEY)); + long discountValue = resultSet.getLong(DISCOUNT_VALUE_KEY); + String voucherTypeStr = resultSet.getString(TYPE_KEY); + LocalDateTime createdAt = resultSet.getTimestamp(CREATED_AT_KEY).toLocalDateTime(); + + return new Voucher(id, createdAt, voucherTypeStr, discountValue); + }; + + private VoucherDomainMapper() { + } + + public static Map voucherToParamMap(Voucher voucher) { + Map paramMap = new HashMap<>(); + paramMap.put(ID_KEY, voucher.getId().toString().getBytes()); + paramMap.put(CREATED_AT_KEY, Timestamp.valueOf(voucher.getCreatedAt())); + paramMap.put(TYPE_KEY, voucher.getTypeName()); + paramMap.put(DISCOUNT_VALUE_KEY, voucher.getDiscountValue()); + return paramMap; + } + + + public static Voucher objectToVoucher(Map voucherObject) { + UUID voucherId = UUID.fromString(String.valueOf(voucherObject.get(ID_KEY))); + LocalDateTime createdAt = LocalDateTime.parse(voucherObject.get(CREATED_AT_KEY)); + String voucherTypeName = String.valueOf(voucherObject.get(TYPE_KEY)); + long discountValue = Long.parseLong(String.valueOf(voucherObject.get(DISCOUNT_VALUE_KEY))); + //TODO: check save format + return new Voucher(voucherId, createdAt, voucherTypeName, discountValue); + } + + public static HashMap voucherToObject(Voucher voucher) { + HashMap voucherObject = new HashMap<>(); + voucherObject.put(ID_KEY, voucher.getId().toString()); + voucherObject.put(CREATED_AT_KEY, voucher.getCreatedAt().toString()); + voucherObject.put(TYPE_KEY, voucher.getTypeName()); + voucherObject.put(DISCOUNT_VALUE_KEY, voucher.getDiscountValue()); + return voucherObject; + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/repository/util/VoucherFileManager.java b/src/main/java/com/programmers/vouchermanagement/voucher/repository/util/VoucherFileManager.java new file mode 100644 index 0000000000..0bd0b74503 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/repository/util/VoucherFileManager.java @@ -0,0 +1,74 @@ +package com.programmers.vouchermanagement.voucher.repository.util; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.programmers.vouchermanagement.properties.AppProperties; +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.util.*; + +import static com.programmers.vouchermanagement.util.Message.FILE_EXCEPTION; +import static com.programmers.vouchermanagement.util.Message.IO_EXCEPTION; +import static com.programmers.vouchermanagement.voucher.repository.util.VoucherDomainMapper.objectToVoucher; +import static com.programmers.vouchermanagement.voucher.repository.util.VoucherDomainMapper.voucherToObject; + +@Component +@Profile("file") +public class VoucherFileManager { + private static final Logger logger = LoggerFactory.getLogger(VoucherFileManager.class); + private final ObjectMapper objectMapper; + private final String filePath; + + public VoucherFileManager(AppProperties appProperties, ObjectMapper objectMapper) { + this.filePath = appProperties.resources().path() + + appProperties.domains().get("voucher").fileName(); + this.objectMapper = objectMapper; + } + + public Map loadFile() { + try { + File file = new File(filePath); + Map[] voucherObjects = objectMapper.readValue(file, Map[].class); + return loadVouchers(voucherObjects); + } catch (IOException e) { + logger.error(IO_EXCEPTION); + throw new UncheckedIOException(e); + } + } + + private Map loadVouchers(Map[] voucherObjects) { + Map vouchers = new HashMap<>(); + Arrays.stream(voucherObjects).forEach(voucherObject -> { + Voucher voucher = objectToVoucher(voucherObject); + vouchers.put(voucher.getId(), voucher); + }); + return vouchers; + } + + public void saveFile(Map vouchers) { + try (FileWriter fileWriter = new FileWriter(filePath)) { + List> voucherObjects = toVoucherObjects(vouchers); + String jsonStr = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(voucherObjects); + fileWriter.write(jsonStr); + fileWriter.flush(); + } catch (Exception e) { + throw new RuntimeException(FILE_EXCEPTION); + } + } + + private List> toVoucherObjects(Map vouchers) { + List> voucherObjects = new ArrayList<>(); + vouchers.values().forEach(voucher -> { + HashMap voucherObject = voucherToObject(voucher); + voucherObjects.add(voucherObject); + }); + return voucherObjects; + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/voucher/service/VoucherService.java b/src/main/java/com/programmers/vouchermanagement/voucher/service/VoucherService.java new file mode 100644 index 0000000000..b55909159f --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/voucher/service/VoucherService.java @@ -0,0 +1,84 @@ +package com.programmers.vouchermanagement.voucher.service; + +import com.programmers.vouchermanagement.voucher.controller.dto.CreateVoucherRequest; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.voucher.domain.vouchertype.VoucherType; +import com.programmers.vouchermanagement.voucher.domain.vouchertype.VoucherTypeManager; +import com.programmers.vouchermanagement.voucher.repository.VoucherRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.*; + +import static com.programmers.vouchermanagement.util.Message.NOT_FOUND_VOUCHER; + +@Service +public class VoucherService { + private final VoucherRepository voucherRepository; + + public VoucherService(VoucherRepository voucherRepository) { + this.voucherRepository = voucherRepository; + } + + @Transactional + public VoucherResponse create(CreateVoucherRequest createVoucherRequest) { + Voucher voucher = new Voucher(createVoucherRequest.typeName(), createVoucherRequest.discountValue()); + voucherRepository.insert(voucher); + return VoucherResponse.from(voucher); + } + + @Transactional(readOnly = true) + public List readAll() { + List vouchers = voucherRepository.findAll(); + + return vouchers.stream() + .map(VoucherResponse::from) + .toList(); + } + + @Transactional(readOnly = true) + public List readAllByCreatedAt(LocalDateTime from, LocalDateTime to) { + List vouchers = voucherRepository.findAllByCreatedAt(from, to); + + return vouchers.stream() + .map(VoucherResponse::from) + .toList(); + } + + @Transactional(readOnly = true) + public VoucherResponse readById(UUID voucherId) { + Optional voucherOptional = voucherRepository + .findById(voucherId); + Voucher voucher = voucherOptional.orElseThrow(() -> new NoSuchElementException(NOT_FOUND_VOUCHER)); + return VoucherResponse.from(voucher); + } + + @Transactional + public VoucherResponse delete(UUID voucherId) { + VoucherResponse voucher = readById(voucherId); + voucherRepository.delete(voucherId); + return voucher; + } + + @Transactional + public void deleteAll() { + voucherRepository.deleteAll(); + } + + @Transactional + public VoucherResponse update(UUID voucherId, CreateVoucherRequest createVoucherRequest) { + // TODO: modify code format + VoucherResponse voucherResponse = readById(voucherId); + Voucher voucher = new Voucher(voucherId, voucherResponse.createdAt(), createVoucherRequest.typeName(), createVoucherRequest.discountValue()); + voucherRepository.update(voucher); + return VoucherResponse.from(voucher); + } + + @Transactional(readOnly = true) + public List readAllByType(String typeName) { + VoucherType voucherType = VoucherTypeManager.get(typeName); + return voucherRepository.findAllByType(voucherType).stream().map(VoucherResponse::from).toList(); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/wallet/controller/WalletController.java b/src/main/java/com/programmers/vouchermanagement/wallet/controller/WalletController.java new file mode 100644 index 0000000000..4349d7f7f1 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/wallet/controller/WalletController.java @@ -0,0 +1,38 @@ +package com.programmers.vouchermanagement.wallet.controller; + +import com.programmers.vouchermanagement.customer.controller.dto.CustomerResponse; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import com.programmers.vouchermanagement.wallet.domain.Ownership; +import com.programmers.vouchermanagement.wallet.service.WalletService; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; + +import java.util.List; +import java.util.UUID; + +@Profile({"console & jdbc"}) +@Controller +public class WalletController { + private final WalletService walletService; + + public WalletController(WalletService walletService) { + this.walletService = walletService; + } + + public void allocate(Ownership ownership) { + walletService.allocate(ownership); + } + + public CustomerResponse readCustomerByVoucherId(UUID voucherId, Model model) { + return walletService.readCustomerByVoucherId(voucherId); + } + + public List readAllVoucherByCustomerId(UUID customerId, Model model) { + return walletService.readAllVoucherByCustomerId(customerId); + } + + public void deleteAllAllocation() { + walletService.deleteAllAllocation(); + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/wallet/controller/WalletThymeleafController.java b/src/main/java/com/programmers/vouchermanagement/wallet/controller/WalletThymeleafController.java new file mode 100644 index 0000000000..9891935163 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/wallet/controller/WalletThymeleafController.java @@ -0,0 +1,49 @@ +package com.programmers.vouchermanagement.wallet.controller; + +import com.programmers.vouchermanagement.customer.controller.dto.CustomerResponse; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import com.programmers.vouchermanagement.wallet.domain.Ownership; +import com.programmers.vouchermanagement.wallet.service.WalletService; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +@Profile({"thyme & jdbc"}) +@RequestMapping("/wallets") +@Controller +public class WalletThymeleafController { + private final WalletService walletService; + + public WalletThymeleafController(WalletService walletService) { + this.walletService = walletService; + } + + @PostMapping + public void allocate(Ownership ownership) { + walletService.allocate(ownership); + } + + @GetMapping("/vouchers/{voucherId}") + public String readCustomerByVoucherId(@PathVariable("voucherId") UUID voucherId, Model model) { + CustomerResponse customer = walletService.readCustomerByVoucherId(voucherId); + model.addAttribute("customers", List.of(customer)); + return "views/customers"; + } + + @GetMapping("/customers/{customerId}") + public String readAllVoucherByCustomerId(@PathVariable("customerId") UUID customerId, Model model) { + List vouchers = walletService.readAllVoucherByCustomerId(customerId); + model.addAttribute("vouchers", List.of(vouchers)); + return "views/vouchers"; + } + + @DeleteMapping + public String deleteAllAllocation() { + walletService.deleteAllAllocation(); + return "redirect:/wallets"; + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/wallet/domain/Ownership.java b/src/main/java/com/programmers/vouchermanagement/wallet/domain/Ownership.java new file mode 100644 index 0000000000..14caf12640 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/wallet/domain/Ownership.java @@ -0,0 +1,6 @@ +package com.programmers.vouchermanagement.wallet.domain; + +import java.util.UUID; + +public record Ownership(UUID voucherId, UUID customerId) { +} diff --git a/src/main/java/com/programmers/vouchermanagement/wallet/repository/WalletJDBCRepository.java b/src/main/java/com/programmers/vouchermanagement/wallet/repository/WalletJDBCRepository.java new file mode 100644 index 0000000000..d805a05667 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/wallet/repository/WalletJDBCRepository.java @@ -0,0 +1,85 @@ +package com.programmers.vouchermanagement.wallet.repository; + +import com.programmers.vouchermanagement.customer.domain.Customer; +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.wallet.domain.Ownership; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.*; + +import static com.programmers.vouchermanagement.customer.repository.util.CustomerDomainMapper.customerRowMapper; +import static com.programmers.vouchermanagement.util.Constant.UPDATE_ONE_FLAG; +import static com.programmers.vouchermanagement.util.Constant.UPDATE_ZERO_FLAG; +import static com.programmers.vouchermanagement.util.DomainMapper.ID_KEY; +import static com.programmers.vouchermanagement.util.Message.*; +import static com.programmers.vouchermanagement.voucher.repository.util.VoucherDomainMapper.voucherRowMapper; +import static com.programmers.vouchermanagement.wallet.repository.util.OwnershipDomainMapper.ownershipToParamMap; +import static com.programmers.vouchermanagement.wallet.repository.util.OwnershipDomainMapper.uuidToParamMap; + +@Profile("jdbc") +@Repository +public class WalletJDBCRepository implements WalletRepository { + private static final Logger logger = LoggerFactory.getLogger(WalletJDBCRepository.class); + private final NamedParameterJdbcTemplate jdbcTemplate; + + public WalletJDBCRepository(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void insert(Ownership ownership) { + int update = jdbcTemplate.update( + "INSERT INTO ownership(voucher_id, customer_id) VALUES (UUID_TO_BIN(:voucher_id), UUID_TO_BIN(:customer_id))", + ownershipToParamMap(ownership)); + if (update != UPDATE_ONE_FLAG) { + logger.error(CAN_NOT_INSERT_OWNERSHIP); + throw new EmptyResultDataAccessException(UPDATE_ONE_FLAG); + } + } + + @Override + public Optional findCustomerByVoucherId(UUID voucherId) { + try { + return Optional.ofNullable(jdbcTemplate.queryForObject( + "SELECT c.* FROM ownership as o JOIN customers as c ON o.customer_id = c.id WHERE o.voucher_id = uuid_to_bin(:id)", + Collections.singletonMap(ID_KEY, voucherId.toString().getBytes()), + customerRowMapper)); + } catch (EmptyResultDataAccessException e) { + return Optional.empty(); + } + } + + @Override + public List findAllVoucherByCustomerId(UUID customerId) { + return jdbcTemplate.query( + "SELECT v.* FROM ownership as o JOIN vouchers as v ON o.voucher_id = v.id WHERE o.customer_id = uuid_to_bin(:id)", + Collections.singletonMap(ID_KEY, customerId.toString().getBytes()), + voucherRowMapper); + } + + @Override + public void delete(UUID voucherId) { + int update = jdbcTemplate.update( + "DELETE FROM ownership WHERE voucher_id = UUID_TO_BIN(:id)", + uuidToParamMap(voucherId)); + if (update != UPDATE_ONE_FLAG) { + logger.error(NOT_FOUND_VOUCHER_ALLOCATION); + throw new NoSuchElementException(NOT_FOUND_VOUCHER_ALLOCATION); + } + } + + @Override + public void deleteAll() { + int update = jdbcTemplate.update( + "TRUNCATE TABLE ownership", + Collections.emptyMap()); + if (update == UPDATE_ZERO_FLAG) { + logger.warn(ALREADY_EMPTY_TABLE); + } + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/wallet/repository/WalletRepository.java b/src/main/java/com/programmers/vouchermanagement/wallet/repository/WalletRepository.java new file mode 100644 index 0000000000..8e2299da30 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/wallet/repository/WalletRepository.java @@ -0,0 +1,21 @@ +package com.programmers.vouchermanagement.wallet.repository; + +import com.programmers.vouchermanagement.customer.domain.Customer; +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.wallet.domain.Ownership; + +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +public interface WalletRepository { + void insert(Ownership ownership); + + List findAllVoucherByCustomerId(UUID customerId); + + void delete(UUID voucherID); + + Optional findCustomerByVoucherId(UUID voucherId); + + void deleteAll(); +} diff --git a/src/main/java/com/programmers/vouchermanagement/wallet/repository/util/OwnershipDomainMapper.java b/src/main/java/com/programmers/vouchermanagement/wallet/repository/util/OwnershipDomainMapper.java new file mode 100644 index 0000000000..2a6fc85b26 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/wallet/repository/util/OwnershipDomainMapper.java @@ -0,0 +1,22 @@ +package com.programmers.vouchermanagement.wallet.repository.util; + +import com.programmers.vouchermanagement.util.DomainMapper; +import com.programmers.vouchermanagement.wallet.domain.Ownership; + +import java.util.HashMap; +import java.util.Map; + +public class OwnershipDomainMapper extends DomainMapper { + public static final String VOUCHER_ID_KEY = "voucher_id"; + public static final String CUSTOMER_ID_KEY = "customer_id"; + + private OwnershipDomainMapper() { + } + + public static Map ownershipToParamMap(Ownership ownership) { + Map paramMap = new HashMap<>(); + paramMap.put(VOUCHER_ID_KEY, ownership.voucherId().toString().getBytes()); + paramMap.put(CUSTOMER_ID_KEY, ownership.customerId().toString().getBytes()); + return paramMap; + } +} diff --git a/src/main/java/com/programmers/vouchermanagement/wallet/service/WalletService.java b/src/main/java/com/programmers/vouchermanagement/wallet/service/WalletService.java new file mode 100644 index 0000000000..59e4cd7ef7 --- /dev/null +++ b/src/main/java/com/programmers/vouchermanagement/wallet/service/WalletService.java @@ -0,0 +1,58 @@ +package com.programmers.vouchermanagement.wallet.service; + +import com.programmers.vouchermanagement.customer.controller.dto.CustomerResponse; +import com.programmers.vouchermanagement.customer.domain.Customer; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.wallet.domain.Ownership; +import com.programmers.vouchermanagement.wallet.repository.WalletRepository; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.*; + +import static com.programmers.vouchermanagement.util.Message.NOT_FOUND_VOUCHER_ALLOCATION; +@Profile("jdbc") +@Service +public class WalletService { + private static final Logger logger = LoggerFactory.getLogger(WalletService.class); + private final WalletRepository walletRepository; + + public WalletService(WalletRepository walletRepository) { + this.walletRepository = walletRepository; + } + + public void allocate(Ownership ownership) { + walletRepository.insert(ownership); + } + + @Transactional(readOnly = true) + public CustomerResponse readCustomerByVoucherId(UUID voucherId) { + Optional customerOptional = walletRepository.findCustomerByVoucherId(voucherId); + Customer customer = customerOptional.orElseThrow(() -> { + logger.error(NOT_FOUND_VOUCHER_ALLOCATION); + return new NoSuchElementException(NOT_FOUND_VOUCHER_ALLOCATION); + }); + return CustomerResponse.from(customer); + } + + @Transactional(readOnly = true) + public List readAllVoucherByCustomerId(UUID customerId) { + List vouchers = walletRepository.findAllVoucherByCustomerId(customerId); + if (vouchers.isEmpty()) return Collections.emptyList(); + return vouchers.stream().map(VoucherResponse::from).toList(); + } + + @Transactional + public void deleteVoucherFromCustomer(UUID voucherId) { + walletRepository.delete(voucherId); + } + + @Transactional + public void deleteAllAllocation() { + walletRepository.deleteAll(); + } +} diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml new file mode 100644 index 0000000000..3a496bde22 --- /dev/null +++ b/src/main/resources/application.yaml @@ -0,0 +1,51 @@ +spring: + config: + activate: + on-profile: api, thyme + mvc: + hidden-method: + filter: + enabled: true +--- +spring: + config: + activate: + on-profile: jdbc + datasource: + url: jdbc:mysql://localhost:3306/prod + username: ${USER} + password: ${PASSWORD} +--- +spring: + config: + activate: + on-profile: file + autoconfigure: + exclude: + - org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration +file: + resources: + path: src/main/resources/ + domains: + customer: + file-name: blacklist.csv + + voucher: + file-name: voucher.json +--- +spring: + config: + activate: + on-profile: test + datasource: + url: jdbc:mysql://localhost:3306/test + username: ${USER} + password: ${PASSWORD} +file: + resources: + path: src/test/resources/ + domains: + customer: + file-name: blacklist.csv + voucher: + file-name: voucher.json diff --git a/src/main/resources/blacklist.csv b/src/main/resources/blacklist.csv new file mode 100644 index 0000000000..195a4a730a --- /dev/null +++ b/src/main/resources/blacklist.csv @@ -0,0 +1,3 @@ +id,name +e8d88c24-668c-4b65-a658-44303bfbb805, 송인재, false +9b46f523-03d4-41b2-a68e-0666f21c6f7d, 익명, true diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000000..0b3435e715 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,25 @@ + + + + + + + + + ERROR + ACCEPT + DENY + + + logs/error-%d{yyyy-MM-dd}.log + + + ${FILE_LOG_PATTERN} + + + + + + + + diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000000..7ca92265aa --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,52 @@ +DROP DATABASE IF EXISTS prod; +CREATE DATABASE prod; +USE prod; +CREATE TABLE customers +( + id BINARY(16) PRIMARY KEY, + name VARCHAR(20) NOT NULL, + black BOOLEAN NOT NULL, + CONSTRAINT name UNIQUE (name) +); +CREATE TABLE vouchers +( + id BINARY(16) PRIMARY KEY, + type ENUM ('FIXED', 'PERCENT') NOT NULL, + discount_value VARCHAR(50) NOT NULL, + created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) +); + +CREATE TABLE ownership +( + voucher_id BINARY(16) PRIMARY KEY, + customer_id BINARY(16), + FOREIGN KEY (voucher_id) REFERENCES vouchers (id) ON DELETE CASCADE, + FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE CASCADE +); + + +DROP DATABASE IF EXISTS test; +CREATE DATABASE test; +USE test; +CREATE TABLE customers +( + id BINARY(16) PRIMARY KEY, + name VARCHAR(20) NOT NULL, + black BOOLEAN NOT NULL, + CONSTRAINT name UNIQUE (name) +); +CREATE TABLE vouchers +( + id BINARY(16) PRIMARY KEY, + type ENUM ('FIXED', 'PERCENT') NOT NULL, + discount_value VARCHAR(50) NOT NULL, + created_at DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6) +); + +CREATE TABLE ownership +( + voucher_id BINARY(16) PRIMARY KEY, + customer_id BINARY(16), + FOREIGN KEY (voucher_id) REFERENCES vouchers (id) ON DELETE CASCADE, + FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE CASCADE +); \ No newline at end of file diff --git a/src/main/resources/templates/customer/customer-new.html b/src/main/resources/templates/customer/customer-new.html new file mode 100644 index 0000000000..b69f9a2c53 --- /dev/null +++ b/src/main/resources/templates/customer/customer-new.html @@ -0,0 +1,33 @@ + + + + + + + + Home + + +

Customers

+

New Customer

+
+
+ + +
+
+ + +
+ + +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/customer/customers.html b/src/main/resources/templates/customer/customers.html new file mode 100644 index 0000000000..6f4420091d --- /dev/null +++ b/src/main/resources/templates/customer/customers.html @@ -0,0 +1,39 @@ + + + + + + + + Home + + +

Customers

+ +

Customer List

+

Customer Blacklist

+ + + + + + + + + + + + + + + +
IDNameBad Customer
+ + \ No newline at end of file diff --git a/src/main/resources/templates/voucher/voucher-detail.html b/src/main/resources/templates/voucher/voucher-detail.html new file mode 100644 index 0000000000..0857ca8706 --- /dev/null +++ b/src/main/resources/templates/voucher/voucher-detail.html @@ -0,0 +1,29 @@ + + + + + + + + Home + + +

Vouchers

+

Voucher Id

+
+ + +
+Ok +Update +
+ +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/voucher/voucher-new.html b/src/main/resources/templates/voucher/voucher-new.html new file mode 100644 index 0000000000..31bd0e32d9 --- /dev/null +++ b/src/main/resources/templates/voucher/voucher-new.html @@ -0,0 +1,38 @@ + + + + + + + + Home + + +

Vouchers

+

New Voucher

+
+
+ + +
+ +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/voucher/voucher-update.html b/src/main/resources/templates/voucher/voucher-update.html new file mode 100644 index 0000000000..6771b7f237 --- /dev/null +++ b/src/main/resources/templates/voucher/voucher-update.html @@ -0,0 +1,39 @@ + + + + + + + + Home + + +

Vouchers

+

+
+
+ + +
+ + Cancel +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/voucher/vouchers.html b/src/main/resources/templates/voucher/vouchers.html new file mode 100644 index 0000000000..3acbd486bd --- /dev/null +++ b/src/main/resources/templates/voucher/vouchers.html @@ -0,0 +1,43 @@ + + + + + + + + Home + + +

Vouchers

+

Voucher List

+
+ Create +
+ + + + + + + + + + + + + + + + + +
IDDiscount Valuevoucher Type
+ Detail + Update +
+ +
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/wallet/wallets.html b/src/main/resources/templates/wallet/wallets.html new file mode 100644 index 0000000000..cb552e3b33 --- /dev/null +++ b/src/main/resources/templates/wallet/wallets.html @@ -0,0 +1,52 @@ + + + + + + + + Home + + +

wallets

+

Voucher Allocation

+
+
Customer
+
+ +
+
Voucher
+
+ +
+ +
+

Voucher Ownership

+ + + + + + + + + + + + + +
voucher idcustomer id
+ + \ No newline at end of file diff --git a/src/main/resources/voucher.json b/src/main/resources/voucher.json new file mode 100644 index 0000000000..02c00045cc --- /dev/null +++ b/src/main/resources/voucher.json @@ -0,0 +1,6 @@ +[ { + "discount_value" : 1233, + "created_at" : "2023-11-06T09:28:01.097908900", + "id" : "b8fcca99-5cdf-4c27-9212-16ec1d35c54e", + "type" : "FIXED" +} ] \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/customer/controller/CustomerRestControllerTest.java b/src/test/java/com/programmers/vouchermanagement/customer/controller/CustomerRestControllerTest.java new file mode 100644 index 0000000000..9986be578a --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/customer/controller/CustomerRestControllerTest.java @@ -0,0 +1,87 @@ +package com.programmers.vouchermanagement.customer.controller; + +import com.programmers.vouchermanagement.customer.service.CustomerService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static com.programmers.vouchermanagement.customer.controller.MvcControllerResource.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(CustomerRestController.class) +@ActiveProfiles("api") +class CustomerRestControllerTest { + @Autowired + MockMvc mockMvc; + @MockBean + CustomerService customerService; + + @Test + @DisplayName("고객 생성을 요청한다.") + void createCustomer() throws Exception { + when(customerService.create(CREATE_CUSTOMER_REQUEST)).thenReturn(CUSTOMER_RESPONSE); + + String response = mockMvc.perform(post("/api/v1/customers") + .contentType(MediaType.APPLICATION_JSON) + .content(OBJECT_MAPPER.writeValueAsString(CREATE_CUSTOMER_REQUEST))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(CUSTOMER_RESPONSE)); + } + + @Test + @DisplayName("모든 고객 조회를 요청한다.") + void readAllCustomers1() throws Exception { + when(customerService.readAll()).thenReturn(CUSTOMERS); + + String response = mockMvc.perform(get("/api/v1/customers")) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(CUSTOMERS)); + } + + @Test + @DisplayName("모든 고객 조회를 요청한다. + 쿼리 스트링 사용(type=all)") + void readAllCustomers2() throws Exception { + when(customerService.readAll()).thenReturn(CUSTOMERS); + + String response = mockMvc.perform(get("/api/v1/customers") + .param("type", "all")) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(CUSTOMERS)); + } + + @Test + @DisplayName("모든 블랙리스트 고객 조회를 요청한다.") + void readAllBlacklist() throws Exception { + when(customerService.readAllBlackCustomer()).thenReturn(BLACKLIST); + + String response = mockMvc.perform(get("/api/v1/customers") + .param("type","blacklist")) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(BLACKLIST)); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/customer/controller/CustomerThymeleafControllerTest.java b/src/test/java/com/programmers/vouchermanagement/customer/controller/CustomerThymeleafControllerTest.java new file mode 100644 index 0000000000..7e2e836c3d --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/customer/controller/CustomerThymeleafControllerTest.java @@ -0,0 +1,80 @@ +package com.programmers.vouchermanagement.customer.controller; + +import com.programmers.vouchermanagement.customer.service.CustomerService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import static com.programmers.vouchermanagement.customer.controller.MvcControllerResource.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(CustomerThymeleafController.class) +@ActiveProfiles("thyme") +class CustomerThymeleafControllerTest { + @Autowired + MockMvc mockMvc; + @MockBean + CustomerService customerService; + + @Test + @DisplayName("고객 생성을 요청한다. 그리고 customers 페이지로 이동한다.") + void createCustomer() throws Exception { + when(customerService.create(CREATE_CUSTOMER_REQUEST)).thenReturn(CUSTOMER_RESPONSE); + + mockMvc.perform(post("/customers/new") + .param("name", "customer") + .param("isBlack", "true")) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/customers")); + } + + @Test + @DisplayName("고객 생성 페이지를 요청한다.") + void viewCreatePage() throws Exception { + mockMvc.perform(get("/customers/new")) + .andExpect(status().isOk()) + .andExpect(view().name("customer/customer-new")); + } + + @Test + @DisplayName("모든 고객 조회 페이지를 요청한다.") + void readAll1() throws Exception { + when(customerService.readAll()).thenReturn(CUSTOMERS); + + mockMvc.perform(get("/customers")) + .andExpect(status().isOk()) + .andExpect(view().name("customer/customers")) + .andExpect(model().attribute("customers", CUSTOMERS)); + } + + @Test + @DisplayName("모든 고객 조회 페이지를 요청한다. + 쿼리 스트링(type=all)") + void readAll2() throws Exception { + when(customerService.readAll()).thenReturn(CUSTOMERS); + + mockMvc.perform(get("/customers") + .param("type", "all")) + .andExpect(status().isOk()) + .andExpect(view().name("customer/customers")) + .andExpect(model().attribute("customers", CUSTOMERS)); + } + + @Test + @DisplayName("블랙리스트 조회 페이지를 요청한다.") + void readAllBlackCustomer() throws Exception { + when(customerService.readAllBlackCustomer()).thenReturn(BLACKLIST); + + mockMvc.perform(get("/customers") + .param("type", "blacklist")) + .andExpect(status().isOk()) + .andExpect(view().name("customer/customers")) + .andExpect(model().attribute("customers", BLACKLIST)); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/customer/controller/MvcControllerResource.java b/src/test/java/com/programmers/vouchermanagement/customer/controller/MvcControllerResource.java new file mode 100644 index 0000000000..a8e2985815 --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/customer/controller/MvcControllerResource.java @@ -0,0 +1,24 @@ +package com.programmers.vouchermanagement.customer.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.programmers.vouchermanagement.customer.controller.dto.CreateCustomerRequest; +import com.programmers.vouchermanagement.customer.controller.dto.CustomerResponse; +import com.programmers.vouchermanagement.customer.domain.Customer; + +import java.util.List; + +public class MvcControllerResource { + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + public static final List CUSTOMERS = List.of( + CustomerResponse.from(new Customer("customer1", false)), + CustomerResponse.from(new Customer("customer2", true)) + ); + public static final List BLACKLIST = List.of( + CustomerResponse.from(new Customer("customer3", true)), + CustomerResponse.from(new Customer("customer4", true)) + ); + public static final CreateCustomerRequest CREATE_CUSTOMER_REQUEST = new CreateCustomerRequest("customer5", true); + + public static final Customer CUSTOMER = new Customer(CREATE_CUSTOMER_REQUEST.name(), CREATE_CUSTOMER_REQUEST.isBlack()); + public static final CustomerResponse CUSTOMER_RESPONSE = CustomerResponse.from(CUSTOMER); +} diff --git a/src/test/java/com/programmers/vouchermanagement/customer/domain/CustomerTest.java b/src/test/java/com/programmers/vouchermanagement/customer/domain/CustomerTest.java new file mode 100644 index 0000000000..6a71c205b8 --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/customer/domain/CustomerTest.java @@ -0,0 +1,23 @@ +package com.programmers.vouchermanagement.customer.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class CustomerTest { + @DisplayName("🚨 고객 이름이 빈칸(or null)이거나 20자가 넘으면 고객이 생성되지 않는다.") + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"", " ", "123456789012345678901"}) + void customerNameNullBlankOver20(String input) { + assertThrows(IllegalArgumentException.class, () -> { + new Customer(input, true); + }); + } + +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/customer/repository/CustomerJDBCRepositoryTest.java b/src/test/java/com/programmers/vouchermanagement/customer/repository/CustomerJDBCRepositoryTest.java new file mode 100644 index 0000000000..670329c4cf --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/customer/repository/CustomerJDBCRepositoryTest.java @@ -0,0 +1,73 @@ +package com.programmers.vouchermanagement.customer.repository; + +import com.programmers.vouchermanagement.customer.domain.Customer; +import com.programmers.vouchermanagement.customer.repository.util.CustomerDomainMapper; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Collections; +import java.util.List; + +import static com.programmers.vouchermanagement.util.DomainMapper.ID_KEY; +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class CustomerJDBCRepositoryTest { + NamedParameterJdbcTemplate jdbcTemplate; + CustomerJDBCRepository customerJDBCRepository; + + @Autowired + CustomerJDBCRepositoryTest(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + customerJDBCRepository = new CustomerJDBCRepository(this.jdbcTemplate); + } + + @Test + @DisplayName("🆗 고객 정보를 저장할 수 있다.") + void insert() { + Customer customer = new Customer("고객4", true); + customerJDBCRepository.insert(customer); + Customer retrievedCustomer = jdbcTemplate + .queryForObject("SELECT * FROM customers WHERE id = UUID_TO_BIN(:id)", Collections.singletonMap(ID_KEY, customer.getId().toString().getBytes()), CustomerDomainMapper.customerRowMapper); + + assertThat(retrievedCustomer).isEqualTo(customer); + } + + @Test + @DisplayName("🆗 블랙리스트를 조회할 수 있다. 단, 블랙 고객이 없는 경우 빈 list가 반환된다.") + void findAllBlackCustomer() { + insertCustomersWithBlackCustomers(); + List customers = customerJDBCRepository.findAllBlackCustomer(); + assertThat(customers).isNotEmpty(); + assertThat(customers.size()).isGreaterThanOrEqualTo(1); + } + + void insertCustomersWithBlackCustomers() { + customerJDBCRepository.insert(new Customer("고객1", false)); + customerJDBCRepository.insert(new Customer("고객2", true)); + customerJDBCRepository.insert(new Customer("고객3", false)); + } + + + @Test + @DisplayName("🚨 블랙 고객이 없는 경우 빈 list가 반환된다.") + void findAllBlackCustomerAndReturnEmpty() { + insertCustomersWithNonBlackCustomers(); + List customers = customerJDBCRepository.findAllBlackCustomer(); + assertThat(customers).isEmpty(); + } + + void insertCustomersWithNonBlackCustomers() { + customerJDBCRepository.insert(new Customer("고객1", false)); + customerJDBCRepository.insert(new Customer("고객2", false)); + customerJDBCRepository.insert(new Customer("고객3", false)); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/voucher/controller/MvcControllerResource.java b/src/test/java/com/programmers/vouchermanagement/voucher/controller/MvcControllerResource.java new file mode 100644 index 0000000000..be94edf81d --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/voucher/controller/MvcControllerResource.java @@ -0,0 +1,36 @@ +package com.programmers.vouchermanagement.voucher.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import com.programmers.vouchermanagement.voucher.controller.dto.CreateVoucherRequest; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import com.programmers.vouchermanagement.voucher.domain.Voucher; + +import java.util.List; +import java.util.UUID; + +public class MvcControllerResource { + public static final ObjectMapper OBJECT_MAPPER = new ObjectMapper() + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); + + public static final List VOUCHERS = List.of( + VoucherResponse.from(new Voucher("FIXED", 3000)), + VoucherResponse.from(new Voucher("PERCENT", 10)) + ); + public static final List FIXED_AMOUNT_VOUCHERS = List.of( + VoucherResponse.from(new Voucher("FIXED", 3000)), + VoucherResponse.from(new Voucher("FIXED", 3000)) + ); + public static final List VOUCHERS_CREATED_NOW = List.of( + VoucherResponse.from(new Voucher("FIXED", 3000)), + VoucherResponse.from(new Voucher("PERCENT", 50)) + ); + public static final CreateVoucherRequest CREATE_VOUCHER_REQUEST = new CreateVoucherRequest("FIXED", 5000); + + public static final Voucher VOUCHER = new Voucher(CREATE_VOUCHER_REQUEST.typeName(), CREATE_VOUCHER_REQUEST.discountValue()); + + public static final UUID VOUCHER_ID = VOUCHER.getId(); + public static final VoucherResponse VOUCHER_RESPONSE = VoucherResponse.from(VOUCHER); +} diff --git a/src/test/java/com/programmers/vouchermanagement/voucher/controller/VoucherRestControllerTest.java b/src/test/java/com/programmers/vouchermanagement/voucher/controller/VoucherRestControllerTest.java new file mode 100644 index 0000000000..306c33cdcf --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/voucher/controller/VoucherRestControllerTest.java @@ -0,0 +1,160 @@ +package com.programmers.vouchermanagement.voucher.controller; + +import com.programmers.vouchermanagement.voucher.controller.dto.CreateVoucherRequest; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import com.programmers.vouchermanagement.voucher.service.VoucherService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static com.programmers.vouchermanagement.voucher.controller.MvcControllerResource.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +@WebMvcTest(VoucherRestController.class) +@ActiveProfiles("api") +class VoucherRestControllerTest { + @Autowired + MockMvc mockMvc; + @MockBean + VoucherService voucherService; + + @Test + @DisplayName("바우처 생성을 요청한다.") + void createVoucher() throws Exception { + when(voucherService.create(CREATE_VOUCHER_REQUEST)).thenReturn(VOUCHER_RESPONSE); + + String response = mockMvc.perform(post("/api/v1/vouchers") + .contentType(MediaType.APPLICATION_JSON) + .content(OBJECT_MAPPER.writeValueAsString(CREATE_VOUCHER_REQUEST))) + .andExpect(status().isCreated()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(VOUCHER_RESPONSE)); + } + + @Test + @DisplayName("모든 바우처 조회를 요청한다.") + void readAllVouchers1() throws Exception { + when(voucherService.readAll()).thenReturn(VOUCHERS); + + String response = mockMvc.perform(get("/api/v1/vouchers")) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(VOUCHERS)); + } + + @Test + @DisplayName("모든 바우처 조회를 요청한다. + 쿼리 스트링(filter=all)") + void readAllVouchers2() throws Exception { + when(voucherService.readAll()).thenReturn(VOUCHERS); + + String response = mockMvc.perform(get("/api/v1/vouchers") + .param("filter", "all")) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(VOUCHERS)); + } + + @Test + @DisplayName("입력 기간 안에 있는 모든 바우처 조회를 요청한다. + 쿼리 스트링(filter=created-at...)") + void readAllByCreatedAt() throws Exception { + LocalDate from = LocalDate.of(2022, 12, 25); + LocalDate to = LocalDate.now(); + when(voucherService.readAllByCreatedAt(any(LocalDateTime.class), any(LocalDateTime.class))) + .thenReturn(VOUCHERS_CREATED_NOW); + + String response = mockMvc.perform(get("/api/v1/vouchers") + .param("filter", "created-at") + .param("from", from.toString()) + .param("to", to.toString())) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(VOUCHERS_CREATED_NOW)); + } + + @Test + @DisplayName("입력 타입에 해당하는 모든 바우처 조회를 요청한다. + 쿼리 스트링(filter=type...)") + void readAllByType() throws Exception { + String typeName = "FIXED"; + when(voucherService.readAllByType(typeName)).thenReturn(FIXED_AMOUNT_VOUCHERS); + + String response = mockMvc.perform(get("/api/v1/vouchers") + .param("filter", "type") + .param("type-name", typeName)) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(FIXED_AMOUNT_VOUCHERS)); + } + + @Test + @DisplayName("바우처 id로 바우처 조회를 요청한다.") + void readVoucherById() throws Exception { + when(voucherService.readById(VOUCHER_ID)).thenReturn(VOUCHER_RESPONSE); + + String response = mockMvc.perform(get("/api/v1/vouchers/" + VOUCHER_ID.toString())) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(VOUCHER_RESPONSE)); + } + + @Test + @DisplayName("바우처 id로 바우처를 삭제한다.") + void deleteVoucher() throws Exception { + when(voucherService.delete(VOUCHER_ID)).thenReturn(VOUCHER_RESPONSE); + + String response = mockMvc.perform(delete("/api/v1/vouchers/" + VOUCHER_ID.toString())) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(VOUCHER_RESPONSE)); + } + + @Test + @DisplayName("바우처 id로 바우처를 업데이트한다.") + void update() throws Exception { + CreateVoucherRequest updateVoucherRequest = new CreateVoucherRequest("FIXED", 100000); + VoucherResponse updatedVoucherResponse = new VoucherResponse(VOUCHER_ID, VOUCHER.getCreatedAt(), CREATE_VOUCHER_REQUEST.typeName(), CREATE_VOUCHER_REQUEST.discountValue()); + when(voucherService.update(VOUCHER_ID, updateVoucherRequest)).thenReturn(updatedVoucherResponse); + + String response = mockMvc.perform(put("/api/v1/vouchers/" + VOUCHER_ID.toString()) + .contentType(MediaType.APPLICATION_JSON) + .content(OBJECT_MAPPER.writeValueAsString(updateVoucherRequest))) + .andExpect(status().isOk()) + .andReturn() + .getResponse() + .getContentAsString(); + + assertThat(response).isEqualTo(OBJECT_MAPPER.writeValueAsString(updatedVoucherResponse)); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/voucher/controller/VoucherThymeleafControllerTest.java b/src/test/java/com/programmers/vouchermanagement/voucher/controller/VoucherThymeleafControllerTest.java new file mode 100644 index 0000000000..c998d7f2bd --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/voucher/controller/VoucherThymeleafControllerTest.java @@ -0,0 +1,95 @@ +package com.programmers.vouchermanagement.voucher.controller; + +import com.programmers.vouchermanagement.voucher.service.VoucherService; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; + +import static com.programmers.vouchermanagement.voucher.controller.MvcControllerResource.*; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(VoucherThymeleafController.class) +@ActiveProfiles("thyme") +class VoucherThymeleafControllerTest { + @Autowired + MockMvc mockMvc; + @MockBean + VoucherService voucherService; + + @Test + @DisplayName("바우처 생성을 요청한다. 그리고 vouchers 페이지로 이동한다.") + void createVoucher() throws Exception { + when(voucherService.create(CREATE_VOUCHER_REQUEST)).thenReturn(VOUCHER_RESPONSE); + + mockMvc.perform(post("/vouchers/new") + .param("typeName", CREATE_VOUCHER_REQUEST.typeName()) + .param("discountValue", Long.toString(CREATE_VOUCHER_REQUEST.discountValue()))) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/vouchers")); + } + + @Test + @DisplayName("바우처 생성 페이지를 요청한다.") + void viewCreatePage() throws Exception { + mockMvc.perform(get("/vouchers/new")) + .andExpect(status().isOk()) + .andExpect(view().name("voucher/voucher-new")); + } + + @Test + @DisplayName("모든 바우처 조회 페이지를 요청한다.") + void viewVouchersPage() throws Exception { + when(voucherService.readAll()).thenReturn(VOUCHERS); + + mockMvc.perform(get("/vouchers")) + .andExpect(status().isOk()) + .andExpect(view().name("voucher/vouchers")) + .andExpect(model().attribute("vouchers", VOUCHERS)); + } + + @Test + @DisplayName("id별 바우처 상세 페이지를 요청한다.") + void viewVoucherByIdPage() throws Exception { + when(voucherService.readById(VOUCHER_ID)).thenReturn(VOUCHER_RESPONSE); + + mockMvc.perform(get("/vouchers/" + VOUCHER_ID.toString())) + .andExpect(status().isOk()) + .andExpect(view().name("voucher/voucher-detail")) + .andExpect(model().attribute("voucher", VOUCHER_RESPONSE)); + } + + @Test + @DisplayName("바우처 삭제를 요청한다.") + void deleteVoucher() throws Exception { + mockMvc.perform(delete("/vouchers/" + VOUCHER_ID.toString())) + .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/vouchers")); + } + + @Test + @DisplayName("바우처 업데이트를 요청한다.") + void update() throws Exception { + mockMvc.perform(put("/vouchers/update/" + VOUCHER_ID.toString()) + .param("typeName", CREATE_VOUCHER_REQUEST.typeName()) + .param("discountValue", Long.toString(CREATE_VOUCHER_REQUEST.discountValue()))) .andExpect(status().is3xxRedirection()) + .andExpect(redirectedUrl("/vouchers")); + } + + @Test + @DisplayName("바우처 업데이트 페이지를 요청한다.") + void viewUpdatePage() throws Exception { + when(voucherService.readById(VOUCHER_ID)).thenReturn(VOUCHER_RESPONSE); + + mockMvc.perform(get("/vouchers/update/" + VOUCHER_ID.toString())) + .andExpect(status().isOk()) + .andExpect(view().name("voucher/voucher-update")) + .andExpect(model().attribute("voucher", VOUCHER_RESPONSE)); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/FixedAmountVoucherTypeTest.java b/src/test/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/FixedAmountVoucherTypeTest.java new file mode 100644 index 0000000000..11f7d45f7b --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/FixedAmountVoucherTypeTest.java @@ -0,0 +1,19 @@ +package com.programmers.vouchermanagement.voucher.domain.vouchertype; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class FixedAmountVoucherTypeTest { + @ParameterizedTest(name = "할인 금액이 {0}인 경우") + @DisplayName("할인 금액이 0미만이거나 100000000 초과일 경우 예외 발생") + @ValueSource(longs = {-1, 100000001}) + void discountValueUnder0AndOver100000000(long input) { + assertThrows(IllegalArgumentException.class, () -> { + VoucherType fixedAmountVoucherType = FixedAmountVoucherType.getInstance(); + fixedAmountVoucherType.validateDiscountValue(input); + }); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/PercentVoucherTypeTest.java b/src/test/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/PercentVoucherTypeTest.java new file mode 100644 index 0000000000..79dfea8f2b --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/PercentVoucherTypeTest.java @@ -0,0 +1,19 @@ +package com.programmers.vouchermanagement.voucher.domain.vouchertype; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +class PercentVoucherTypeTest { + @ParameterizedTest(name = "할인 금액이 {0}인 경우") + @DisplayName("할인 금액이 0미만이거나 100 초과일 경우 예외 발생") + @ValueSource(longs = {-1, 101}) + void discountValueUnder0AndOver100(long input) { + assertThrows(IllegalArgumentException.class, () -> { + VoucherType percentVoucherType = PercentVoucherType.getInstance(); + percentVoucherType.validateDiscountValue(input); + }); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/VoucherTypeManagerTest.java b/src/test/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/VoucherTypeManagerTest.java new file mode 100644 index 0000000000..2a70702cd7 --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/voucher/domain/vouchertype/VoucherTypeManagerTest.java @@ -0,0 +1,37 @@ +package com.programmers.vouchermanagement.voucher.domain.vouchertype; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class VoucherTypeManagerTest { + + @Test + @DisplayName("타입 이름을 FIXED로 입력한 경우 FixedAmountVoucherType 반환") + void typeNameIsFIXED() { + VoucherType voucherType = VoucherTypeManager.get("FIXED"); + + assertThat(voucherType.getClass()).isEqualTo(FixedAmountVoucherType.class); + } + + @Test + @DisplayName("타입 이름을 PERCENT로 입력한 경우 PercentVoucherType 반환") + void typeNameIsPERCENT() { + VoucherType voucherType = VoucherTypeManager.get("PERCENT"); + + assertThat(voucherType.getClass()).isEqualTo(PercentVoucherType.class); + } + + @ParameterizedTest(name = "타입 입력이 {0}인 경우") + @DisplayName("타입 이름을 잘못입력한 경우 예외 발생") + @ValueSource(strings = {"ANOTHER", "FIIXED", "PERRCENT"}) + void typeNameIsNotCorrect(String input) { + assertThrows(IllegalArgumentException.class, () -> { + VoucherTypeManager.get(input); + }); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/voucher/repository/VoucherInMemoryRepositoryTest.java b/src/test/java/com/programmers/vouchermanagement/voucher/repository/VoucherInMemoryRepositoryTest.java new file mode 100644 index 0000000000..a3b4c77a62 --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/voucher/repository/VoucherInMemoryRepositoryTest.java @@ -0,0 +1,51 @@ +package com.programmers.vouchermanagement.voucher.repository; + +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +class VoucherInMemoryRepositoryTest { + VoucherInMemoryRepository voucherInMemoryRepository = new VoucherInMemoryRepository(); + + @Test + @DisplayName("🆗 바우처를 아이디로 삭제할 수 있다.") + void deleteVoucherSucceed() { + Voucher voucher = new Voucher("FIXED", 5555); + voucherInMemoryRepository.insert(voucher); + + voucherInMemoryRepository.delete(voucher.getId()); + + assertThat(voucherInMemoryRepository.findById(voucher.getId()).isEmpty()).isTrue(); + } + + @Test + @DisplayName("🚨 없는 바우처를 삭제하면 실패한다.") + void deleteNonExistVoucherFail() { + UUID NonExistVoucherId = UUID.randomUUID(); + + assertThrows(RuntimeException.class, () -> voucherInMemoryRepository.delete(NonExistVoucherId)); + } + + @Test + @DisplayName("🆗 바우처를 업데이트 할 수 있다.") + void updateVoucherSucceed() { + Voucher voucher = new Voucher("FIXED", 5555); + voucherInMemoryRepository.insert(voucher); + + Voucher updatedVoucher = new Voucher(voucher.getId(), voucher.getCreatedAt(), "PERCENT", 100); + voucherInMemoryRepository.update(updatedVoucher); + + Optional retrievedVoucher = voucherInMemoryRepository.findById(voucher.getId()); + assertThat(retrievedVoucher.isEmpty()).isFalse(); + assertThat(retrievedVoucher.get().getDiscountValue()).isEqualTo(updatedVoucher.getDiscountValue()); + assertThat(retrievedVoucher.get().getTypeName()).isEqualTo(updatedVoucher.getTypeName()); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/voucher/repository/VoucherJDBCRepositoryTest.java b/src/test/java/com/programmers/vouchermanagement/voucher/repository/VoucherJDBCRepositoryTest.java new file mode 100644 index 0000000000..be907cce27 --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/voucher/repository/VoucherJDBCRepositoryTest.java @@ -0,0 +1,130 @@ +package com.programmers.vouchermanagement.voucher.repository; + +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@JdbcTest +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class VoucherJDBCRepositoryTest { + private final static UUID NON_EXISTENT_VOUCHER_ID = UUID.randomUUID(); + NamedParameterJdbcTemplate jdbcTemplate; + VoucherJDBCRepository voucherJDBCRepository; + + @Autowired + VoucherJDBCRepositoryTest(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + voucherJDBCRepository = new VoucherJDBCRepository(this.jdbcTemplate); + } + + @Test + @DisplayName("🆗 고정 금액 할인 바우처를 추가할 수 있다.") + void saveFixedAmountVoucher() { + Voucher voucher = new Voucher("FIXED", 1000); + voucherJDBCRepository.insert(voucher); + + Optional retrievedVoucher = voucherJDBCRepository.findById(voucher.getId()); + + assertThat(retrievedVoucher).isNotEmpty(); + assertThat(retrievedVoucher.get().getId()).isEqualTo(voucher.getId()); + } + + @Test + @DisplayName("🆗 퍼센트 할인 바우처를 추가할 수 있다.") + void savePercentVoucher() { + Voucher voucher = new Voucher("PERCENT", 50); + voucherJDBCRepository.insert(voucher); + + Optional retrievedVoucher = voucherJDBCRepository.findById(voucher.getId()); + + assertThat(retrievedVoucher.isEmpty()).isFalse(); + assertThat(retrievedVoucher.get().getId()).isEqualTo(voucher.getId()); + } + + @Test + @DisplayName("🆗 모든 바우처를 조회할 수 있다. 단, 없다면 빈 list를 반환한다.") + void findAllVoucher() { + for (int i = 1; i < 6; i++) + voucherJDBCRepository.insert(new Voucher("PERCENT", i)); + + List vouchers = voucherJDBCRepository.findAll(); + + assertThat(vouchers.size()).isGreaterThanOrEqualTo(5); + } + + @Test + @DisplayName("🆗 바우처를 아이디로 조회할 수 있다.") + void findVoucherById() { + Voucher voucher = new Voucher("FIXED", 1234); + voucherJDBCRepository.insert(voucher); + + Optional retrievedVoucher = voucherJDBCRepository.findById(voucher.getId()); + + assertThat(retrievedVoucher.isPresent()).isTrue(); + assertThat(retrievedVoucher.get().getId()).isEqualTo(voucher.getId()); + assertThat(retrievedVoucher.get().getDiscountValue()).isEqualTo(voucher.getDiscountValue()); + assertThat(retrievedVoucher.get().getTypeName()).isEqualTo(voucher.getTypeName()); + } + + @Test + @DisplayName("🚨 해당하는 바우처가 없다면, 바우처를 아이디로 조회할 수 없다.") + void findNonExistentVoucherById() { + Optional retrievedVoucher = voucherJDBCRepository.findById(NON_EXISTENT_VOUCHER_ID); + + assertThat(retrievedVoucher.isEmpty()).isTrue(); + } + + @Test + @DisplayName("🆗 바우처를 아이디로 삭제할 수 있다.") + void deleteVoucher() { + Voucher voucher = new Voucher("FIXED", 5555); + voucherJDBCRepository.insert(voucher); + + voucherJDBCRepository.delete(voucher.getId()); + + assertThat(voucherJDBCRepository.findById(voucher.getId()).isEmpty()).isTrue(); + } + + @Test + @DisplayName("🚨 해당하는 바우처가 없다면, 바우처를 아이디로 삭제할 수 없다.") + void deleteNonExistentVoucher() { + assertThrows(RuntimeException.class, () -> voucherJDBCRepository.delete(NON_EXISTENT_VOUCHER_ID)); + } + + @Test + @DisplayName("🆗 바우처를 업데이트 할 수 있다.") + void updateVoucher() { + Voucher voucher = new Voucher("FIXED", 5555); + voucherJDBCRepository.insert(voucher); + + Voucher updatedVoucher = new Voucher(voucher.getId(), voucher.getCreatedAt(), "PERCENT", 100); + voucherJDBCRepository.update(updatedVoucher); + + Optional retrievedVoucher = voucherJDBCRepository.findById(voucher.getId()); + assertThat(retrievedVoucher.isEmpty()).isFalse(); + assertThat(retrievedVoucher.get().getDiscountValue()).isEqualTo(updatedVoucher.getDiscountValue()); + assertThat(retrievedVoucher.get().getTypeName()).isEqualTo(updatedVoucher.getTypeName()); + } + + @Test + @DisplayName("🚨 해당하는 바우처가 없다면, 바우처를 업데이트 할 수 없다.") + void updateNonExistentVoucher() { + assertThrows(NoSuchElementException.class, () -> voucherJDBCRepository.update(new Voucher(NON_EXISTENT_VOUCHER_ID, LocalDateTime.now(), "PERCENT", 100))); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/voucher/service/VoucherServiceTest.java b/src/test/java/com/programmers/vouchermanagement/voucher/service/VoucherServiceTest.java new file mode 100644 index 0000000000..96eed8c028 --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/voucher/service/VoucherServiceTest.java @@ -0,0 +1,99 @@ +package com.programmers.vouchermanagement.voucher.service; + +import com.programmers.vouchermanagement.voucher.controller.dto.CreateVoucherRequest; +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.voucher.repository.VoucherRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(SpringExtension.class) +class VoucherServiceTest { + @InjectMocks + VoucherService voucherService; + @Mock + VoucherRepository voucherRepository; + @Mock + UUID mockId; + @Mock + LocalDateTime mockLocalDateTime; + @Mock + Voucher mockVoucher; + + @Test + @DisplayName("단위 테스트를 위해 Service에 Mock 객체(Repository) 주입") + void initTest() { + assertThat(voucherRepository).isNotNull(); + assertThat(voucherService).isNotNull(); + } + + @Test + @DisplayName("고정 할인 금액 바우처 객체를 생성할 수 있다.") + void createFixedAmountVoucher() { + CreateVoucherRequest createVoucherRequest = new CreateVoucherRequest("FIXED", 1000); + voucherService.create(createVoucherRequest); + + verify(voucherRepository, times(1)).insert(any(Voucher.class)); + } + + @Test + @DisplayName("퍼센트 할인 바우처 객체를 생성할 수 있다.") + void createPercentDiscountVoucher() { + CreateVoucherRequest createVoucherRequest = new CreateVoucherRequest("PERCENT", 100); + voucherService.create(createVoucherRequest); + + verify(voucherRepository, times(1)).insert(any(Voucher.class)); + } + + @Test + @DisplayName("모든 바우처를 조회할 수 있다.") + void readAllVouchers() { + voucherService.readAll(); + + when(voucherRepository.findAll()).thenReturn(new ArrayList<>()); + + verify(voucherRepository, times(1)).findAll(); + } + + @Test + @DisplayName("바우처를 id로 조회할 수 있다.") + void readVoucherById() { + when(voucherRepository.findById(mockId)).thenReturn(Optional.of(mockVoucher)); + + voucherService.readById(mockId); + + verify(voucherRepository, times(1)).findById(mockId); + } + + @Test + @DisplayName("바우처를 id로 삭제할 수 있다.") + void deleteVoucher() { + when(voucherRepository.findById(mockId)).thenReturn(Optional.of(mockVoucher)); + + voucherService.delete(mockId); + + verify(voucherRepository, times(1)).delete(any(UUID.class)); + } + + @Test + @DisplayName("바우처를 업데이트할 수 있다.") + void updateVoucher() { + when(voucherRepository.findById(mockId)).thenReturn(Optional.of(new Voucher(mockId, mockLocalDateTime, "FIXED", 130))); + + voucherService.update(mockId, new CreateVoucherRequest("FIXED", 100)); + + verify(voucherRepository, times(1)).update(any(Voucher.class)); + } +} \ No newline at end of file diff --git a/src/test/java/com/programmers/vouchermanagement/wallet/repository/WalletJDBCRepositoryTest.java b/src/test/java/com/programmers/vouchermanagement/wallet/repository/WalletJDBCRepositoryTest.java new file mode 100644 index 0000000000..c0e998c8b2 --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/wallet/repository/WalletJDBCRepositoryTest.java @@ -0,0 +1,188 @@ +package com.programmers.vouchermanagement.wallet.repository; + +import com.programmers.vouchermanagement.customer.domain.Customer; +import com.programmers.vouchermanagement.customer.repository.CustomerJDBCRepository; +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.voucher.repository.VoucherJDBCRepository; +import com.programmers.vouchermanagement.wallet.domain.Ownership; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.test.context.ActiveProfiles; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@JdbcTest +@ActiveProfiles("test") +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class WalletJDBCRepositoryTest { + private final static UUID NON_EXISTENT_VOUCHER_ID = UUID.randomUUID(); + private final static UUID NON_EXISTENT_CUSTOMER_ID = UUID.randomUUID(); + + NamedParameterJdbcTemplate jdbcTemplate; + WalletJDBCRepository walletJDBCRepository; + VoucherJDBCRepository voucherJDBCRepository; + CustomerJDBCRepository customerJDBCRepository; + + @Autowired + WalletJDBCRepositoryTest(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = new NamedParameterJdbcTemplate(jdbcTemplate); + walletJDBCRepository = new WalletJDBCRepository(this.jdbcTemplate); + voucherJDBCRepository = new VoucherJDBCRepository(this.jdbcTemplate); + customerJDBCRepository = new CustomerJDBCRepository(this.jdbcTemplate); + } + + @Test + @Order(1) + @DisplayName("🆗 의존성 주입 테스트.") + void injectionTest() { + assertAll(() -> assertThat(walletJDBCRepository).isNotNull(), () -> assertThat(voucherJDBCRepository).isNotNull(), () -> assertThat(customerJDBCRepository).isNotNull()); + } + + @Test + @DisplayName("🆗 고객에게 바우처를 할당할 수 있다.") + void save() { + Voucher voucher = new Voucher("FIXED", 333); + Customer customer = new Customer("바우처 주인", false); + + voucherJDBCRepository.insert(voucher); + customerJDBCRepository.insert(customer); + + Ownership newOwnership = new Ownership(voucher.getId(), customer.getId()); + walletJDBCRepository.insert(newOwnership); + } + + @Test + @DisplayName("🚨 이미 할당된 바우처라면, 고객에게 바우처를 할당할 수 없다.") + void saveAllocatedVoucher() { + Voucher voucher = new Voucher("FIXED", 333); + Customer customer = new Customer("바우처를 가진 고객", false); + + voucherJDBCRepository.insert(voucher); + customerJDBCRepository.insert(customer); + walletJDBCRepository.insert(new Ownership(voucher.getId(), customer.getId())); + + Customer customer2 = new Customer("바우처를 가지지 못하는 고객", false); + customerJDBCRepository.insert(customer2); + + assertThrows(RuntimeException.class, () -> walletJDBCRepository.insert(new Ownership(voucher.getId(), customer2.getId()))); + } + + @Test + @DisplayName("🚨 고객 id에 해당하는 고객이 없다면, 고객에게 바우처를 할당할 수 없다.") + void saveNonExistentCustomer() { + Voucher voucher = new Voucher("FIXED", 333); + voucherJDBCRepository.insert(voucher); + + assertThrows(RuntimeException.class, () -> walletJDBCRepository.insert(new Ownership(voucher.getId(), NON_EXISTENT_CUSTOMER_ID))); + } + + @Test + @DisplayName("🚨 바우처 id에 해당하는 바우처가 없다면, 바우처를 고객에게 할당할 수 없다.") + void saveNonExistentVoucher() { + Customer customer = new Customer("바우처를 가지지 못한 고객", false); + customerJDBCRepository.insert(customer); + + assertThrows(RuntimeException.class, () -> walletJDBCRepository.insert(new Ownership(NON_EXISTENT_VOUCHER_ID, customer.getId()))); + } + + @Test + @DisplayName("🚨 id에 해당하는 바우처와 고객이 모두 없다면, 바우처를 고객에게 할당할 수 없다.") + void saveNonExistentBoth() { + assertThrows(RuntimeException.class, () -> walletJDBCRepository.insert(new Ownership(NON_EXISTENT_VOUCHER_ID, NON_EXISTENT_CUSTOMER_ID))); + } + + @Test + @DisplayName("🆗 고객 id로 고객이 가진 바우처들을 가져올 수 있다.") + void findAllVoucherByCustomerId() { + Customer customer = new Customer("조회하려는 바우처들의 주인", false); + customerJDBCRepository.insert(customer); + + for (int i = 1; i < 6; i++) { + Voucher voucher = new Voucher("FIXED", i); + voucherJDBCRepository.insert(voucher); + walletJDBCRepository.insert(new Ownership(voucher.getId(), customer.getId())); + } + + // If the customer don't have any voucher, then return empty list. + assertThat(walletJDBCRepository.findAllVoucherByCustomerId(customer.getId()).isEmpty()).isFalse(); + } + + @Test + @DisplayName("🚨 고객에 대한 할당 정보가 없다면, 고객이 가진 바우처들을 가져올 수 없다.") + void findAllVoucherByNonExistentCustomerId() { + assertThat(walletJDBCRepository.findAllVoucherByCustomerId(NON_EXISTENT_CUSTOMER_ID).isEmpty()).isTrue(); + } + + @Test + @DisplayName("🆗 바우처 id를 통해 할당 정보를 삭제할 수 있다. 단, 바우처 자체는 삭제되지 않는다.") + void delete() { + Voucher voucher = new Voucher("FIXED", 333); + Customer customer = new Customer("1개의 삭제될 바우처를 가진 주인", false); + + voucherJDBCRepository.insert(voucher); + customerJDBCRepository.insert(customer); + walletJDBCRepository.insert(new Ownership(voucher.getId(), customer.getId())); + + walletJDBCRepository.delete(voucher.getId()); + + assertThat(walletJDBCRepository.findCustomerByVoucherId(voucher.getId()).isEmpty()).isTrue(); + assertThat(voucherJDBCRepository.findById(voucher.getId()).isPresent()).isTrue(); + } + + @Test + @DisplayName("🚨 바우처에 대한 할당 정보가 없다면, 함께 저장된 고객 id와 바우처 id 정보를 삭제할 수 없다.") + void deleteNonAllocatedVoucher() { + Voucher voucher = new Voucher("FIXED", 333); + voucherJDBCRepository.insert(voucher); + + assertThrows(RuntimeException.class, () -> walletJDBCRepository.delete(voucher.getId())); + } + + @Test + @DisplayName("🆗 바우처 id로 바우처를 가진 고객 정보를 가져올 수 있다.") + void findCustomerByVoucherId() { + Voucher voucher = new Voucher("FIXED", 555); + Customer customer = new Customer("조회될 고객", false); + + voucherJDBCRepository.insert(voucher); + customerJDBCRepository.insert(customer); + walletJDBCRepository.insert(new Ownership(voucher.getId(), customer.getId())); + + Optional retrievedCustomer = walletJDBCRepository.findCustomerByVoucherId(voucher.getId()); + + assertThat(retrievedCustomer.isPresent()).isTrue(); + assertThat(retrievedCustomer.get().getId()).isEqualTo(customer.getId()); + } + + @Test + @DisplayName("🚨 바우처에 대한 할당 정보가 없다면, 바우처를 가진 고객 정보를 가져올 수 없다.") + void findCustomerByNonExistentVoucherId() { + assertThat(walletJDBCRepository.findCustomerByVoucherId(NON_EXISTENT_VOUCHER_ID).isEmpty()).isTrue(); + } + + @Test + @DisplayName("🆗 바우처 자체를 삭제하면, 바우처 소유 정보가 사라진다.") + void autoDeleteAfterVoucherDelete() { + Voucher voucher = new Voucher("FIXED", 555); + Customer customer = new Customer("삭제될 바우처를 가진 고객", false); + + voucherJDBCRepository.insert(voucher); + customerJDBCRepository.insert(customer); + walletJDBCRepository.insert(new Ownership(voucher.getId(), customer.getId())); + + voucherJDBCRepository.delete(voucher.getId()); + + assertThat(walletJDBCRepository.findCustomerByVoucherId(voucher.getId()).isEmpty()).isTrue(); + } +} diff --git a/src/test/java/com/programmers/vouchermanagement/wallet/service/WalletServiceTest.java b/src/test/java/com/programmers/vouchermanagement/wallet/service/WalletServiceTest.java new file mode 100644 index 0000000000..97911efca1 --- /dev/null +++ b/src/test/java/com/programmers/vouchermanagement/wallet/service/WalletServiceTest.java @@ -0,0 +1,91 @@ +package com.programmers.vouchermanagement.wallet.service; + +import com.programmers.vouchermanagement.customer.controller.dto.CustomerResponse; +import com.programmers.vouchermanagement.customer.domain.Customer; +import com.programmers.vouchermanagement.voucher.controller.dto.VoucherResponse; +import com.programmers.vouchermanagement.voucher.domain.Voucher; +import com.programmers.vouchermanagement.wallet.domain.Ownership; +import com.programmers.vouchermanagement.wallet.repository.WalletRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import java.util.List; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.*; + +@ExtendWith(SpringExtension.class) +class WalletServiceTest { + @InjectMocks + WalletService walletService; + @Mock + WalletRepository walletRepository; + @Mock + UUID mockUUID; + @Mock + Customer mockCustomer; + @Mock + Voucher mockVoucher; + @Mock + Ownership ownership; + + @Test + @DisplayName("🆗 단위 테스트를 위해 Service에 Mock 객체(Repository) 주입") + void initTest() { + assertThat(walletRepository).isNotNull(); + assertThat(walletService).isNotNull(); + } + + @Test + @DisplayName("🆗 바우처를 고객에게 할당할 수 있다.") + void allocate() { + walletService.allocate(ownership); + + verify(walletRepository, times(1)).insert(ownership); + } + + @Test + @DisplayName("🆗 고객 id로 고객이 가진 바우처를 조회할 수 있다.") + void findAllVoucherByCustomerId() { + List mockVoucherList = mock(List.class); + when(walletRepository.findAllVoucherByCustomerId(mockUUID)).thenReturn(mockVoucherList); + + assertThat(walletService.readAllVoucherByCustomerId(mockUUID)).isEqualTo(mockVoucherList.stream().map(VoucherResponse::from).toList()); + + verify(walletRepository, times(1)).findAllVoucherByCustomerId(mockUUID); + } + + @Test + @DisplayName("🆗 고객에게 할당한 바우처를 제거할 수 있다.") + void deleteVoucherFromCustomer() { + walletService.deleteVoucherFromCustomer(mockUUID); + + verify(walletRepository, times(1)).delete(mockUUID); + } + + @Test + @DisplayName("🆗 바우처 id로 바우처가 할당된 고객을 조회할 수 있다.") + void findCustomerByVoucherId() { + when(walletRepository.findCustomerByVoucherId(mockUUID)).thenReturn(Optional.of(mockCustomer)); + + assertThat(walletService.readCustomerByVoucherId(mockUUID)).isEqualTo(CustomerResponse.from(mockCustomer)); + verify(walletRepository, times(1)).findCustomerByVoucherId(mockUUID); + } + + @Test + @DisplayName("🚨 바우처가 없으면, 바우처가 할당된 고객을 조회할 수 없다.") + void findCustomerByNonExistVoucherId() { + when(walletRepository.findCustomerByVoucherId(mockUUID)).thenReturn(Optional.empty()); + + assertThrows(NoSuchElementException.class, () -> walletService.readCustomerByVoucherId(mockUUID)); + verify(walletRepository, times(1)).findCustomerByVoucherId(mockUUID); + } +} \ No newline at end of file diff --git a/src/test/resources/blacklist.csv b/src/test/resources/blacklist.csv new file mode 100644 index 0000000000..79dfd50f04 --- /dev/null +++ b/src/test/resources/blacklist.csv @@ -0,0 +1 @@ +id,name \ No newline at end of file diff --git a/src/test/resources/voucher.json b/src/test/resources/voucher.json new file mode 100644 index 0000000000..0637a088a0 --- /dev/null +++ b/src/test/resources/voucher.json @@ -0,0 +1 @@ +[] \ No newline at end of file