diff --git a/.github/release.yml b/.github/release.yml new file mode 100644 index 0000000..eca6961 --- /dev/null +++ b/.github/release.yml @@ -0,0 +1,33 @@ +name-template: "Store v$RESOLVED_VERSION" +tag-template: "v$RESOLVED_VERSION" +categories: + - title: "๐Ÿ†• ์ƒˆ๋กœ์šด ๊ธฐ๋Šฅ์ด ์ถ”๊ฐ€๋˜์—ˆ์–ด์š”!" + label: "โœจ Feature" + - title: "๐Ÿž ์ž์ž˜ํ•œ ๋ฒ„๊ทธ๋ฅผ ์ˆ˜์ •ํ–ˆ์Šต๋‹ˆ๋‹ค." + label: "๐Ÿž Bugfix" + - title: "๐Ÿซถ๐Ÿป ์•ฑ ์‚ฌ์šฉ์„ฑ ๊ฐœ์„ ์— ํž˜์ผ์Šต๋‹ˆ๋‹ค." + label: "๐Ÿซถ๐Ÿป Improvement" + - title: "๐Ÿ› ๏ธ ๋” ๋‚˜์€ ์ฝ”๋“œ๋ฅผ ์œ„ํ•ด ๋…ธ๋ ฅํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค." + labels: + - "๐Ÿ”จ Refactor" + - "โš™๏ธ Setting" + - title: "ETC" + labels: + - "*" +change-template: "* $TITLE (#$NUMBER) by @$AUTHOR" +change-title-escapes: '\<*_&#@`' +exclude-labels: + - "Main" +version-resolver: + major: + labels: + - "Major" + minor: + labels: + - "Minor" + patch: + labels: + - "Patch" + default: patch +template: | + $CHANGES diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..fea18ed --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,25 @@ +name: PR build + + +on: + pull_request: + branches: [ develop ] # develop branch์— PR์„ ๋ณด๋‚ผ ๋•Œ ์‹คํ–‰ + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Set up JDK 11 + uses: actions/setup-java@v1 + with: + java-version: 11 + + # Gradle wrapper ํŒŒ์ผ ์‹คํ–‰ ๊ถŒํ•œ์ฃผ๊ธฐ + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Gradle test๋ฅผ ์‹คํ–‰ํ•œ๋‹ค + - name: build gradle + run: ./gradlew clean build \ No newline at end of file diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..9aa4fe9 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,26 @@ +name: develop deployment +on: + push: + branches: + - 'develop' +jobs: + deployment: + environment: develop + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DEV_DOCKER_ID }} + password: ${{ secrets.DEV_DOCKER_PW }} + - name: deploy + id: docker_build-linux + uses: docker/build-push-action@v2 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: nowgnas/bb:store diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0137f74 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,61 @@ +name: Release Tag + +on: + push: + branches: + - main + +jobs: + update_release_draft: + runs-on: ubuntu-latest + steps: + - uses: release-drafter/release-drafter@v5 + with: + config-name: release.yml + env: + GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }} + + deployment: + name: Setup, Build, and Deploy + runs-on: ubuntu-latest + permissions: + packages: write + contents: write + id-token: write + steps: + - name: Bump version and push tag + id: tag_version + uses: mathieudutour/github-tag-action@v6.1 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + + # Set the new tag as a variable + - name: Set New Tag Variable + id: set_new_tag + run: echo "NEW_TAG=${{ steps.tag_version.outputs.new_tag }}" >> $GITHUB_ENV + + - name: Create a GitHub release + uses: ncipollo/release-action@v1 + with: + tag: ${{ steps.tag_version.outputs.new_tag }} + name: Release ${{ steps.tag_version.outputs.new_tag }} + body: ${{ steps.tag_version.outputs.changelog }} + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v1 + + - name: Login to DockerHub + uses: docker/login-action@v1 + with: + username: ${{ secrets.DEV_DOCKER_ID }} + password: ${{ secrets.DEV_DOCKER_PW }} + + - name: deploy + id: docker_build + uses: docker/build-push-action@v2 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: nowgnas/bb-store:${{ steps.tag_version.outputs.new_tag }}, nowgnas/bb-store:latest diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..30b757f --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ +data.sql + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ +*.DS_Store +docker.sh \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..35473d6 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,18 @@ +FROM adoptopenjdk:11-hotspot AS builder + +ENV USE_PROFILE local + +COPY gradlew . +COPY gradle gradle +COPY build.gradle . +COPY settings.gradle . +COPY src src +RUN chmod +x ./gradlew +RUN ./gradlew clean bootJar + +FROM adoptopenjdk:11-hotspot +COPY --from=builder build/libs/*.jar app.jar + +ENTRYPOINT ["java", "-jar", \ + "-Dspring.profiles.active=${USE_PROFILE}", \ + "/app.jar"] \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..4bd2ff8 --- /dev/null +++ b/build.gradle @@ -0,0 +1,201 @@ +buildscript { + ext { + queryDslVersion = "5.0.0" + } +} + +plugins { + id 'java' + id 'org.springframework.boot' version '2.7.17' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' + id "com.ewerk.gradle.plugins.querydsl" version "1.0.10" + id "jacoco" +} + +group = 'kr.bb' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '11' +} + +configurations { + compileOnly { + extendsFrom annotationProcessor + } +} + +repositories { + mavenCentral() +} + +ext { + set('springCloudVersion', "2021.0.8") +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + implementation 'org.springframework.boot:spring-boot-starter-validation' + + implementation 'org.springframework.boot:spring-boot-starter-actuator' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.cloud:spring-cloud-starter-config' + implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client' + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation "org.springframework.cloud:spring-cloud-starter-bus-kafka" + implementation 'org.springframework.kafka:spring-kafka' + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' + implementation 'io.micrometer:micrometer-registry-prometheus:1.12.2' + + runtimeOnly 'com.h2database:h2' + runtimeOnly 'com.mysql:mysql-connector-j' + + // querydsl + implementation "com.querydsl:querydsl-jpa:${queryDslVersion}" + implementation "com.querydsl:querydsl-apt:${queryDslVersion}" + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation("it.ozimov:embedded-redis:0.7.2") + implementation 'org.redisson:redisson-spring-boot-starter:3.17.0' + + // testContainers + testImplementation "org.junit.jupiter:junit-jupiter:5.8.1" + testImplementation "org.testcontainers:junit-jupiter:1.16.3" + + // resilience4j + implementation 'org.springframework.boot:spring-boot-starter-aop' + implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j' + + // sqs + implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.4.RELEASE' + implementation 'org.springframework.cloud:spring-cloud-aws-messaging:2.2.4.RELEASE' + implementation 'software.amazon.awssdk:sns:2.21.37' + + implementation group: 'io.github.lotteon-maven', name: 'blooming-blooms-utils', version: '202401160417' +} + +dependencyManagement { + imports { + mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" + } +} + +tasks.named('bootBuildImage') { + builder = 'paketobuildpacks/builder-jammy-base:latest' +} + +tasks.named('test') { + useJUnitPlatform() +} + +// querydsl ์‚ฌ์šฉํ•  ๊ฒฝ๋กœ ์ง€์ •ํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์ง€์ •ํ•œ ๋ถ€๋ถ„์€ .gitignore์— ํฌํ•จ๋˜๋ฏ€๋กœ git์— ์˜ฌ๋ผ๊ฐ€์ง€ ์•Š์Šต๋‹ˆ๋‹ค. +def querydslDir = "$buildDir/generated/'querydsl'" + +// JPA ์‚ฌ์šฉ์—ฌ๋ถ€ ๋ฐ ์‚ฌ์šฉ ๊ฒฝ๋กœ ์„ค์ • +querydsl { + jpa = true + querydslSourcesDir = querydslDir +} + +// build์‹œ ์‚ฌ์šฉํ•  sourceSet ์ถ”๊ฐ€ ์„ค์ • +sourceSets { + main.java.srcDir querydslDir +} + +// querydsl ์ปดํŒŒ์ผ ์‹œ ์‚ฌ์šฉํ•  ์˜ต์…˜ ์„ค์ • +compileQuerydsl { + options.annotationProcessorPath = configurations.querydsl +} + +// querydsl์ด compileClassPath๋ฅผ ์ƒ์†ํ•˜๋„๋ก ์„ค์ • +configurations { + compileOnly { + extendsFrom annotationProcessor + } + querydsl.extendsFrom compileClasspath +} + +// jacoco +jacoco { + toolVersion = '0.8.5' +} + +jacocoTestCoverageVerification { + violationRules { + rule { + enabled = true + element = 'CLASS' + + limit { + counter = 'CLASS' + value = 'COVEREDRATIO' + minimum = 0.80 + } + + def Qdomains = [] + for(qPattern in '**/QA'..'**/QZ'){ + Qdomains.add(qPattern + '*') + } + afterEvaluate { + classDirectories.setFrom( + files(classDirectories.files.collect{ + fileTree(dir:it,excludes:[ + "**/*Exception.class", + "**/*Controller.class", + "**/*Dto.class", + "**/*Request.class", + "**/*Response.class", + "**/*Id.class", + "**/*Facade.class", + "**/*Publisher.class", + "**/util/*", + "**/*Config.class", + "**/*Application*" + ]+Qdomains) + }) + ) + } + } + } +} + +test { + jacoco { + destinationFile = file("$buildDir/jacoco/jacoco.exec") + } + useJUnitPlatform() + finalizedBy 'jacocoTestReport' +} + +jacocoTestReport { + reports { + html.required = true + } + def Qdomains = [] + for(qPattern in '**/QA'..'**/QZ'){ + Qdomains.add(qPattern + '*') + } + afterEvaluate { + classDirectories.setFrom( + files(classDirectories.files.collect{ + fileTree(dir:it,excludes:[ + "**/*Exception.class", + "**/*Controller.class", + "**/*Dto.class", + "**/*Request.class", + "**/*Response.class", + "**/*Id.class", + "**/*Facade.class", + "**/*Publisher.class", + "**/util/*", + "**/*Config.class", + "**/*Application*" + ]+Qdomains) + }) + ) + } + finalizedBy 'jacocoTestCoverageVerification' +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..7f93135 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 0000000..3fa8f86 --- /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.4-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..1aa94a4 --- /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=SC2039,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=SC2039,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, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +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 0000000..6689b85 --- /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/lombok.config b/lombok.config new file mode 100644 index 0000000..8f7e8aa --- /dev/null +++ b/lombok.config @@ -0,0 +1 @@ +lombok.addLombokGeneratedAnnotation = true \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..22f92b1 --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'store' diff --git a/src/main/java/kr/bb/store/StoreServiceApplication.java b/src/main/java/kr/bb/store/StoreServiceApplication.java new file mode 100644 index 0000000..367f491 --- /dev/null +++ b/src/main/java/kr/bb/store/StoreServiceApplication.java @@ -0,0 +1,17 @@ +package kr.bb.store; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.netflix.eureka.EnableEurekaClient; +import org.springframework.kafka.annotation.EnableKafka; + +@EnableKafka +@EnableEurekaClient +@SpringBootApplication +public class StoreServiceApplication { + + public static void main(String[] args) { + SpringApplication.run(StoreServiceApplication.class, args); + } + +} diff --git a/src/main/java/kr/bb/store/client/ProductFeignClient.java b/src/main/java/kr/bb/store/client/ProductFeignClient.java new file mode 100644 index 0000000..1480a3d --- /dev/null +++ b/src/main/java/kr/bb/store/client/ProductFeignClient.java @@ -0,0 +1,42 @@ +package kr.bb.store.client; + +import bloomingblooms.domain.flower.FlowerDto; +import bloomingblooms.domain.product.StoreSubscriptionProductId; +import bloomingblooms.response.CommonResponse; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@FeignClient(name = "product-service", url = "${endpoint.product-service}") +public interface ProductFeignClient { + org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(ProductFeignClient.class); + + @GetMapping("/client/flowers") + CommonResponse> getFlowers(); + + @CircuitBreaker( + name = "getSubscriptionProductId", + fallbackMethod = "getSubscriptionProductIdFallback" + ) + @GetMapping("/client/store") + CommonResponse getSubscriptionProductId(@RequestParam(name="store-id") Long storeId); + + // subscriptionProductId๋Š” ์ •๊ธฐ๊ตฌ๋… ์‹ ์ฒญ ๋ฒ„ํŠผ์„ ์œ„ํ•ด ๋ฐ›์•„์˜ค๋Š” ๋ฐ์ดํ„ฐ + // ํ•ด๋‹น ๊ฐ’์„ ๋ฐ›์•„์˜ค์ง€ ๋ชปํ•˜๋”๋ผ๋„ ๋‹ค๋ฅธ ๊ฐ€๊ฒŒ์ •๋ณด๋“ค์€ ๊ณ ๊ฐ์ด ๋ณผ ์ˆ˜ ์žˆ๋Š”๊ฒŒ ๋” ํ•ฉ๋ฆฌ์ ์ด๋ผ๊ณ  ํŒ€์ ์œผ๋กœ ํŒ๋‹จํ•จ + // subscriptionProductId๊ฐ€ null์ผ ๋•Œ ํ”„๋ก ํŠธ์—์„œ๋Š” ํ•ด๋‹น ๋ฒ„ํŠผ์„ ํด๋ฆญํ•  ์ˆ˜ ์—†๋Š” ๋น„ํ™œ์„ฑํ™” ์ƒํƒœ๋กœ ๋ณด์ด๊ฒŒ ๋จ + default CommonResponse getSubscriptionProductIdFallback(Exception e) { + log.error(e.toString()); + log.warn("{}'s Request of '{}' failed. request will return fallback data", + "ProductFeignClient", "getSubscriptionProductIdFallback"); + return CommonResponse.builder() + .data(StoreSubscriptionProductId.builder() + .subscriptionProductId(null) + .build() + ) + .message("data from circuit") + .build(); + } +} diff --git a/src/main/java/kr/bb/store/client/StoreLikeFeignClient.java b/src/main/java/kr/bb/store/client/StoreLikeFeignClient.java new file mode 100644 index 0000000..ed7e2a1 --- /dev/null +++ b/src/main/java/kr/bb/store/client/StoreLikeFeignClient.java @@ -0,0 +1,37 @@ +package kr.bb.store.client; + +import bloomingblooms.response.CommonResponse; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@FeignClient(name = "storeLike-service", url = "${endpoint.storeLike-service}") +public interface StoreLikeFeignClient { + org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StoreLikeFeignClient.class); + + @CircuitBreaker( + name = "getStoreLikes", + fallbackMethod = "getStoreLikesFallback" + ) + @PostMapping("/client/likes/stores") + CommonResponse> getStoreLikes( + @RequestHeader(value = "userId") Long userId, @RequestBody List storeIds); + + default CommonResponse> getStoreLikesFallback( + @RequestHeader(value = "userId") Long userId, @RequestBody List storeIds, Exception e) { + log.error(e.toString()); + log.warn("{}'s Request of '{}' failed. request will return fallback data", + "StoreLikeFeignClient", "getStoreLikes"); + Map data = storeIds.stream().collect(Collectors.toMap(id -> id, id -> false)); + return CommonResponse.>builder() + .data(data) + .message("data from circuit") + .build(); + } +} diff --git a/src/main/java/kr/bb/store/client/StoreSubscriptionFeignClient.java b/src/main/java/kr/bb/store/client/StoreSubscriptionFeignClient.java new file mode 100644 index 0000000..bac33d1 --- /dev/null +++ b/src/main/java/kr/bb/store/client/StoreSubscriptionFeignClient.java @@ -0,0 +1,38 @@ +package kr.bb.store.client; + +import bloomingblooms.response.CommonResponse; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@FeignClient(name = "storeSubscription-service", url = "${endpoint.storeSubscription-service}") +public interface StoreSubscriptionFeignClient { + org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(StoreSubscriptionFeignClient.class); + + @CircuitBreaker( + name = "getStoreSubscriptions", + fallbackMethod = "getStoreSubscriptionsFallback" + ) + @PostMapping("/client/order-query/subs/lists") + CommonResponse> getStoreSubscriptions( + @RequestHeader(value = "userId") Long userId, @RequestBody List storeIds); + + default CommonResponse> getStoreSubscriptionsFallback( + @RequestHeader(value = "userId") Long userId, List storeIds, Exception e) { + log.error(e.toString()); + log.warn("{}'s Request of '{}' failed. request will return fallback data", + "StoreSubscriptionFeignClient", "getStoreSubscriptions"); + Map data = storeIds.stream().collect(Collectors.toMap(id -> id, id -> false)); + return CommonResponse.>builder() + .data(data) + .message("data from circuit") + .build(); + } + +} diff --git a/src/main/java/kr/bb/store/client/UserFeignClient.java b/src/main/java/kr/bb/store/client/UserFeignClient.java new file mode 100644 index 0000000..ba9b7c9 --- /dev/null +++ b/src/main/java/kr/bb/store/client/UserFeignClient.java @@ -0,0 +1,19 @@ +package kr.bb.store.client; + + +import bloomingblooms.response.CommonResponse; +import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; + +@FeignClient(name = "user-service", url="${endpoint.user-service}") +public interface UserFeignClient { + + @CircuitBreaker( + name = "getStoreSubscriptions" + ) + @GetMapping("/client/users/{userId}/phone-number") + CommonResponse getPhoneNumber(@PathVariable(name = "userId") Long userId); + +} diff --git a/src/main/java/kr/bb/store/client/dto/StoreInfoDto.java b/src/main/java/kr/bb/store/client/dto/StoreInfoDto.java new file mode 100644 index 0000000..03f4147 --- /dev/null +++ b/src/main/java/kr/bb/store/client/dto/StoreInfoDto.java @@ -0,0 +1,51 @@ +package kr.bb.store.client.dto; + +import kr.bb.store.domain.store.entity.Store; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreInfoDto { + private Long storeId; + private String storeCode; + private String storeName; + private String detailInfo; + private String storeThumbnailInfo; + private Double averageRating; + private String phoneNumber; + private String accountNumber; + private String bank; + + public static StoreInfoDto fromEntity(Store store) { + return StoreInfoDto.builder() + .storeId(store.getId()) + .storeCode(store.getStoreCode()) + .storeName(store.getStoreName()) + .detailInfo(store.getDetailInfo()) + .storeThumbnailInfo(store.getStoreThumbnailImage()) + .averageRating(store.getAverageRating()) + .phoneNumber(store.getPhoneNumber()) + .accountNumber(store.getAccountNumber()) + .bank(store.getBank()) + .build(); + } + + public bloomingblooms.domain.store.StoreInfoDto toCommonDto() { + return bloomingblooms.domain.store.StoreInfoDto.builder() + .storeId(storeId) + .storeCode(storeCode) + .storeName(storeName) + .detailInfo(detailInfo) + .storeThumbnailInfo(storeThumbnailInfo) + .averageRating(averageRating) + .phoneNumber(phoneNumber) + .accountNumber(accountNumber) + .bank(bank) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/client/dto/StoreNameAndAddressDto.java b/src/main/java/kr/bb/store/client/dto/StoreNameAndAddressDto.java new file mode 100644 index 0000000..4e611a7 --- /dev/null +++ b/src/main/java/kr/bb/store/client/dto/StoreNameAndAddressDto.java @@ -0,0 +1,31 @@ +package kr.bb.store.client.dto; + +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreNameAndAddressDto { + private String storeName; + private String storeAddress; + + public static StoreNameAndAddressDto of(Store store, StoreAddress storeAddress) { + return StoreNameAndAddressDto.builder() + .storeName(store.getStoreName()) + .storeAddress(storeAddress.getAddress() + " " +storeAddress.getDetailAddress()) + .build(); + } + + public bloomingblooms.domain.store.StoreNameAndAddressDto toCommonDto() { + return bloomingblooms.domain.store.StoreNameAndAddressDto.builder() + .storeName(storeName) + .storeAddress(storeAddress) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/config/AWSConfig.java b/src/main/java/kr/bb/store/config/AWSConfig.java new file mode 100644 index 0000000..7c8da9e --- /dev/null +++ b/src/main/java/kr/bb/store/config/AWSConfig.java @@ -0,0 +1,41 @@ +package kr.bb.store.config; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.sqs.AmazonSQSAsync; +import com.amazonaws.services.sqs.AmazonSQSAsyncClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider; + +@Configuration +public class AWSConfig { + @Value("${cloud.aws.credentials.ACCESS_KEY_ID}") + private String accessKeyId; + + @Value("${cloud.aws.credentials.SECRET_ACCESS_KEY}") + private String secretAccessKey; + + @Value("${cloud.aws.region.static}") + private String region; + + public AwsCredentialsProvider getAwsCredentials() { + AwsBasicCredentials awsBasicCredentials = + AwsBasicCredentials.create(accessKeyId, secretAccessKey); + return () -> awsBasicCredentials; + } + + @Primary + @Bean + public AmazonSQSAsync amazonSQSAsync() { + BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKeyId, secretAccessKey); + return AmazonSQSAsyncClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials)) + .build(); + } + +} \ No newline at end of file diff --git a/src/main/java/kr/bb/store/config/CacheConfig.java b/src/main/java/kr/bb/store/config/CacheConfig.java new file mode 100644 index 0000000..02b943a --- /dev/null +++ b/src/main/java/kr/bb/store/config/CacheConfig.java @@ -0,0 +1,54 @@ +package kr.bb.store.config; + + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +import java.time.Duration; + +@Configuration +@EnableCaching +@Profile({"!test"}) +public class CacheConfig { + + @Bean + public CacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) { + RedisCacheConfiguration storeListWithPagingConfig = RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofDays(30)) + .disableCachingNullValues() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) + ); + + return RedisCacheManager.RedisCacheManagerBuilder + .fromConnectionFactory(redisConnectionFactory) + .cacheDefaults(redisCacheConfiguration()) + .withCacheConfiguration("store-list-with-paging",storeListWithPagingConfig) + .build(); + } + + @Bean + public RedisCacheConfiguration redisCacheConfiguration() { + return RedisCacheConfiguration.defaultCacheConfig() + .entryTtl(Duration.ofDays(30)) + .disableCachingNullValues() + .serializeKeysWith( + RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()) + ) + .serializeValuesWith( + RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()) + ); + } +} diff --git a/src/main/java/kr/bb/store/config/FeignConfig.java b/src/main/java/kr/bb/store/config/FeignConfig.java new file mode 100644 index 0000000..fd3d681 --- /dev/null +++ b/src/main/java/kr/bb/store/config/FeignConfig.java @@ -0,0 +1,9 @@ +package kr.bb.store.config; + +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.context.annotation.Configuration; + +@EnableFeignClients(basePackages = "kr.bb.store") +@Configuration +public class FeignConfig { +} diff --git a/src/main/java/kr/bb/store/config/JpaAuditingConfig.java b/src/main/java/kr/bb/store/config/JpaAuditingConfig.java new file mode 100644 index 0000000..73f041e --- /dev/null +++ b/src/main/java/kr/bb/store/config/JpaAuditingConfig.java @@ -0,0 +1,9 @@ +package kr.bb.store.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; + +@EnableJpaAuditing +@Configuration +public class JpaAuditingConfig { +} diff --git a/src/main/java/kr/bb/store/config/RedisConfig.java b/src/main/java/kr/bb/store/config/RedisConfig.java new file mode 100644 index 0000000..5bdfaed --- /dev/null +++ b/src/main/java/kr/bb/store/config/RedisConfig.java @@ -0,0 +1,47 @@ +package kr.bb.store.config; + +import org.redisson.Redisson; +import org.redisson.api.RedissonClient; +import org.redisson.config.Config; +import org.redisson.spring.data.connection.RedissonConnectionFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + @Value("${spring.data.redis.host}") + private String host; + @Value("${spring.data.redis.port}") + private int port; + @Value("${spring.data.redis.password}") + private String password; + + private static final String REDISSON_HOST_PREFIX = "redis://"; + + @Bean + public RedissonClient redissonClient() { + Config config = new Config(); + config.useSingleServer().setAddress(REDISSON_HOST_PREFIX + host + ":" + port); + config.useSingleServer().setPassword(password); + return Redisson.create(config); + } + + @Bean + public RedisConnectionFactory redisConnectionFactory(RedissonClient redissonClient) { + return new RedissonConnectionFactory(redissonClient); + } + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setKeySerializer(new StringRedisSerializer()); + redisTemplate.setValueSerializer(new StringRedisSerializer()); + redisTemplate.setConnectionFactory(redisConnectionFactory); + return redisTemplate; + } + +} diff --git a/src/main/java/kr/bb/store/config/Resilience4jConfig.java b/src/main/java/kr/bb/store/config/Resilience4jConfig.java new file mode 100644 index 0000000..f219fe2 --- /dev/null +++ b/src/main/java/kr/bb/store/config/Resilience4jConfig.java @@ -0,0 +1,32 @@ +package kr.bb.store.config; + +import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig; +import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + +@Configuration +public class Resilience4jConfig { + @Bean + public CircuitBreakerConfig circuitBreakerConfig() { + return CircuitBreakerConfig.custom() + .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED) + .slidingWindowSize(10) // ์Šฌ๋ผ์ด๋”ฉ ์œˆ๋„์šฐ ํฌ๊ธฐ + .failureRateThreshold(20) // ์„œํ‚ท์„ openํ•  ์‹คํŒจ ๋น„์œจ + .slowCallDurationThreshold(Duration.ofSeconds(2)) // ๋Š๋ฆฐ์‘๋‹ต ์กฐ๊ฑด + .slowCallRateThreshold(20) // ์„œํ‚ท์„ openํ•  ๋Š๋ฆฐ์‘๋‹ต ๋น„์œจ + .minimumNumberOfCalls(5) // ์„œํ‚ท ํ™•์ธ์„ ์œ„ํ•œ ์ตœ์†Œ ์š”์ฒญ ๊ฐœ์ˆ˜ + .waitDurationInOpenState(Duration.ofMinutes(5)) // open์—์„œ half-open์ด ๋˜๊ธฐ๊นŒ์ง€์˜ ์‹œ๊ฐ„ + .maxWaitDurationInHalfOpenState(Duration.ofMinutes(5)) // half-open์ƒํƒœ์˜ ์ตœ๋Œ€์œ ์ง€๊ธฐ๊ฐ„ + .permittedNumberOfCallsInHalfOpenState(3) // half-open์ƒํƒœ์—์„œ ๋ฐ›์•„๋“ค์ผ ์š”์ฒญ ๊ฐœ์ˆ˜ + .build(); + } + + @Bean + public CircuitBreakerRegistry circuitBreakerRegistry(CircuitBreakerConfig circuitBreakerConfig) { + return CircuitBreakerRegistry.of(circuitBreakerConfig); + } + +} diff --git a/src/main/java/kr/bb/store/domain/cargo/controller/CargoController.java b/src/main/java/kr/bb/store/domain/cargo/controller/CargoController.java new file mode 100644 index 0000000..a4bdc2e --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/controller/CargoController.java @@ -0,0 +1,26 @@ +package kr.bb.store.domain.cargo.controller; + +import bloomingblooms.response.CommonResponse; +import kr.bb.store.domain.cargo.controller.request.StockModifyRequest; +import kr.bb.store.domain.cargo.controller.response.RemainingStocksResponse; +import kr.bb.store.domain.cargo.facade.CargoFacade; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequiredArgsConstructor +public class CargoController { + private final CargoFacade cargoFacade; + + @PutMapping("/{storeId}/flowers/stocks") + public void modifyAllStocks(@PathVariable Long storeId, + @RequestBody StockModifyRequest stockModifyRequest) { + cargoFacade.modifyAllStocksWithLock(storeId,stockModifyRequest.getStockModifyDtos()); + } + + @GetMapping("/{storeId}/flowers/stocks") + public CommonResponse getAllStocks(@PathVariable Long storeId){ + return CommonResponse.success(cargoFacade.getAllStocks(storeId)); + } + +} diff --git a/src/main/java/kr/bb/store/domain/cargo/controller/CargoFeignController.java b/src/main/java/kr/bb/store/domain/cargo/controller/CargoFeignController.java new file mode 100644 index 0000000..3409541 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/controller/CargoFeignController.java @@ -0,0 +1,28 @@ +package kr.bb.store.domain.cargo.controller; + +import bloomingblooms.domain.flower.StockChangeDto; +import bloomingblooms.response.CommonResponse; +import kr.bb.store.domain.cargo.facade.CargoFacade; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@CrossOrigin(origins = "*") +@RestController +@RequiredArgsConstructor +@RequestMapping("/client/stores/flowers/stocks") +public class CargoFeignController { + private final CargoFacade cargoFacade; + @PutMapping("/add") + public CommonResponse addStock(@RequestBody List stockChangeDtos) { + cargoFacade.plusStocksWithLock(stockChangeDtos); + return CommonResponse.success(null); + } + + @PutMapping("/subtract") + public CommonResponse subtractStock(@RequestBody List stockChangeDtos) { + cargoFacade.minusStocksWithLock(stockChangeDtos); + return CommonResponse.success(null); + } +} diff --git a/src/main/java/kr/bb/store/domain/cargo/controller/request/StockModifyRequest.java b/src/main/java/kr/bb/store/domain/cargo/controller/request/StockModifyRequest.java new file mode 100644 index 0000000..38086c9 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/controller/request/StockModifyRequest.java @@ -0,0 +1,17 @@ +package kr.bb.store.domain.cargo.controller.request; + +import kr.bb.store.domain.cargo.dto.StockModifyDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StockModifyRequest { + List stockModifyDtos; +} diff --git a/src/main/java/kr/bb/store/domain/cargo/controller/response/RemainingStocksResponse.java b/src/main/java/kr/bb/store/domain/cargo/controller/response/RemainingStocksResponse.java new file mode 100644 index 0000000..2c3dc96 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/controller/response/RemainingStocksResponse.java @@ -0,0 +1,23 @@ +package kr.bb.store.domain.cargo.controller.response; + +import kr.bb.store.domain.cargo.dto.StockInfoDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RemainingStocksResponse { + List stockInfoDtos; + + public static RemainingStocksResponse from(List stockInfoDtos) { + return RemainingStocksResponse.builder() + .stockInfoDtos(stockInfoDtos) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/cargo/dto/StockInfoDto.java b/src/main/java/kr/bb/store/domain/cargo/dto/StockInfoDto.java new file mode 100644 index 0000000..d00b388 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/dto/StockInfoDto.java @@ -0,0 +1,28 @@ +package kr.bb.store.domain.cargo.dto; + +import kr.bb.store.domain.cargo.entity.FlowerCargo; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StockInfoDto { + private Long flowerId; + private String name; + private List data; + + public static StockInfoDto fromEntity(FlowerCargo flowerCargo) { + return StockInfoDto + .builder() + .flowerId(flowerCargo.getId().getFlowerId()) + .name(flowerCargo.getFlowerName()) + .data(List.of(flowerCargo.getStock())) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/cargo/dto/StockModifyDto.java b/src/main/java/kr/bb/store/domain/cargo/dto/StockModifyDto.java new file mode 100644 index 0000000..89a1389 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/dto/StockModifyDto.java @@ -0,0 +1,15 @@ +package kr.bb.store.domain.cargo.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StockModifyDto { + private Long flowerId; + private Long stock; +} diff --git a/src/main/java/kr/bb/store/domain/cargo/entity/FlowerCargo.java b/src/main/java/kr/bb/store/domain/cargo/entity/FlowerCargo.java new file mode 100644 index 0000000..381e84a --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/entity/FlowerCargo.java @@ -0,0 +1,30 @@ +package kr.bb.store.domain.cargo.entity; + +import kr.bb.store.domain.common.entity.BaseEntity; +import kr.bb.store.domain.store.entity.Store; +import lombok.*; +import org.hibernate.annotations.DynamicInsert; + +import javax.persistence.*; + +@Entity +@Getter +@Builder +@DynamicInsert +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class FlowerCargo extends BaseEntity { + @EmbeddedId + private FlowerCargoId id; + + @MapsId("storeId") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="store_id") + private Store store; + + @Column(nullable = false, columnDefinition = "bigint default 0") + private Long stock; + + private String flowerName; + +} diff --git a/src/main/java/kr/bb/store/domain/cargo/entity/FlowerCargoId.java b/src/main/java/kr/bb/store/domain/cargo/entity/FlowerCargoId.java new file mode 100644 index 0000000..0923623 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/entity/FlowerCargoId.java @@ -0,0 +1,34 @@ +package kr.bb.store.domain.cargo.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Getter +@Builder +@Embeddable +@NoArgsConstructor +@AllArgsConstructor +public class FlowerCargoId implements Serializable{ + private Long storeId; + + private Long flowerId; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + FlowerCargoId that = (FlowerCargoId) o; + return Objects.equals(storeId, that.storeId) && Objects.equals(flowerId, that.flowerId); + } + + @Override + public int hashCode() { + return Objects.hash(storeId, flowerId); + } + +} diff --git a/src/main/java/kr/bb/store/domain/cargo/exception/FlowerCargoNotFoundException.java b/src/main/java/kr/bb/store/domain/cargo/exception/FlowerCargoNotFoundException.java new file mode 100644 index 0000000..3123902 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/exception/FlowerCargoNotFoundException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.cargo.exception; + +import kr.bb.store.exception.CustomException; + +public class FlowerCargoNotFoundException extends CustomException { + public static final String MESSAGE = "ํ•ด๋‹น ๊ฐ€๊ฒŒ๋Š” ํ•ด๋‹น ๊ฝƒ ์žฌ๊ณ ๋ฅผ ๋“ฑ๋กํ•˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค"; + + public FlowerCargoNotFoundException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/cargo/exception/LockInterruptedException.java b/src/main/java/kr/bb/store/domain/cargo/exception/LockInterruptedException.java new file mode 100644 index 0000000..f658efc --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/exception/LockInterruptedException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.cargo.exception; + +import kr.bb.store.exception.CustomException; + +public class LockInterruptedException extends CustomException { + private static final String MESSAGE = "๋ฝ ํš๋“ ์‹œ๋„์ค‘ ์ธํ„ฐ๋ŸฝํŠธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."; + + public LockInterruptedException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/cargo/exception/StockCannotBeNegativeException.java b/src/main/java/kr/bb/store/domain/cargo/exception/StockCannotBeNegativeException.java new file mode 100644 index 0000000..6b72452 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/exception/StockCannotBeNegativeException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.cargo.exception; + +import kr.bb.store.exception.CustomException; + +public class StockCannotBeNegativeException extends CustomException { + private static final String MESSAGE = "์žฌ๊ณ ๋Š” ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; + + public StockCannotBeNegativeException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/cargo/exception/StockChangeFailedException.java b/src/main/java/kr/bb/store/domain/cargo/exception/StockChangeFailedException.java new file mode 100644 index 0000000..3505a08 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/exception/StockChangeFailedException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.cargo.exception; + +import kr.bb.store.exception.CustomException; + +public class StockChangeFailedException extends CustomException { + public static final String MESSAGE = "์žฌ๊ณ ์ฐจ๊ฐ ๋ณ€๊ฒฝ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค."; + + public StockChangeFailedException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/cargo/facade/CargoFacade.java b/src/main/java/kr/bb/store/domain/cargo/facade/CargoFacade.java new file mode 100644 index 0000000..e17daeb --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/facade/CargoFacade.java @@ -0,0 +1,88 @@ +package kr.bb.store.domain.cargo.facade; + +import bloomingblooms.domain.flower.StockChangeDto; +import bloomingblooms.domain.notification.NotificationKind; +import kr.bb.store.domain.cargo.controller.response.RemainingStocksResponse; +import kr.bb.store.domain.cargo.dto.StockModifyDto; +import kr.bb.store.domain.cargo.exception.LockInterruptedException; +import kr.bb.store.domain.cargo.exception.StockChangeFailedException; +import kr.bb.store.domain.cargo.service.CargoService; +import kr.bb.store.message.OrderStatusSQSPublisher; +import kr.bb.store.message.OutOfStockSQSPublisher; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static kr.bb.store.util.RedisUtils.makeRedissonKey; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CargoFacade { + private final CargoService cargoService; + private final RedissonClient redissonClient; + private final OutOfStockSQSPublisher outOfStockSQSPublisher; + private final OrderStatusSQSPublisher orderStatusSQSPublisher; + + @Value("${redisson.lock.wait-second}") + private Integer waitSecond; + + @Value("${redisson.lock.lease-second}") + private Integer leaseSecond; + + public void modifyAllStocksWithLock(Long storeId, List stockModifyDtos) { + RLock lock = redissonClient.getLock(makeRedissonKey(storeId)); + try { + boolean available = lock.tryLock(waitSecond, leaseSecond, TimeUnit.SECONDS); + if(!available) { + throw new StockChangeFailedException(); + } + + cargoService.modifyAllStocks(storeId, stockModifyDtos); + log.info("stock in {} modified" ,storeId); + } catch (InterruptedException e){ + throw new LockInterruptedException(); + } finally { + if(lock.isLocked() && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } + } + + public void plusStocksWithLock(List stockChangeDtos) { + try { + Set inSufficientStores = cargoService.plusStockCounts(stockChangeDtos); + inSufficientStores.forEach(outOfStockSQSPublisher::publish); + } catch (Exception e) { + Long userId = stockChangeDtos.get(0).getUserId(); + String phoneNumber = stockChangeDtos.get(0).getPhoneNumber(); + orderStatusSQSPublisher.publish(userId, phoneNumber, NotificationKind.OUT_OF_STOCK); + + throw e; + } + } + + public void minusStocksWithLock(List stockChangeDtos) { + try { + Set inSufficientStores = cargoService.minusStockCounts(stockChangeDtos); + inSufficientStores.forEach(outOfStockSQSPublisher::publish); + } catch (Exception e) { + Long userId = stockChangeDtos.get(0).getUserId(); + String phoneNumber = stockChangeDtos.get(0).getPhoneNumber(); + orderStatusSQSPublisher.publish(userId, phoneNumber, NotificationKind.OUT_OF_STOCK); + throw e; + } + } + + public RemainingStocksResponse getAllStocks(Long storeId) { + return cargoService.getAllStocks(storeId); + } + +} diff --git a/src/main/java/kr/bb/store/domain/cargo/repository/FlowerCargoRepository.java b/src/main/java/kr/bb/store/domain/cargo/repository/FlowerCargoRepository.java new file mode 100644 index 0000000..053b86a --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/repository/FlowerCargoRepository.java @@ -0,0 +1,31 @@ +package kr.bb.store.domain.cargo.repository; + +import kr.bb.store.domain.cargo.entity.FlowerCargo; +import kr.bb.store.domain.cargo.entity.FlowerCargoId; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface FlowerCargoRepository extends JpaRepository { + List findAllByStoreId(Long storeId); + + @Modifying + @Query("update FlowerCargo f set f.stock = :stock " + + "where f.id.storeId = :storeId and f.id.flowerId = :flowerId") + void modifyStock(@Param("storeId") Long storeId, @Param("flowerId") Long flowerId, @Param("stock") long stock); + + @Modifying + @Query("update FlowerCargo f set f.stock = f.stock + :stock " + + "where f.id.storeId = :storeId and f.id.flowerId = :flowerId") + void plusStock(@Param("storeId") Long storeId, @Param("flowerId") Long flowerId, @Param("stock") long stock); + + @Modifying + @Query("update FlowerCargo f set f.stock = f.stock - :stock " + + "where f.id.storeId = :storeId and f.id.flowerId = :flowerId") + void minusStock(@Param("storeId") Long storeId, @Param("flowerId") Long flowerId, @Param("stock") long stock); + + +} diff --git a/src/main/java/kr/bb/store/domain/cargo/service/CargoService.java b/src/main/java/kr/bb/store/domain/cargo/service/CargoService.java new file mode 100644 index 0000000..d754fa9 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/cargo/service/CargoService.java @@ -0,0 +1,184 @@ +package kr.bb.store.domain.cargo.service; + + +import bloomingblooms.domain.flower.FlowerDto; +import bloomingblooms.domain.flower.StockChangeDto; +import kr.bb.store.domain.cargo.controller.response.RemainingStocksResponse; +import kr.bb.store.domain.cargo.dto.StockInfoDto; +import kr.bb.store.domain.cargo.dto.StockModifyDto; +import kr.bb.store.domain.cargo.entity.FlowerCargo; +import kr.bb.store.domain.cargo.entity.FlowerCargoId; +import kr.bb.store.domain.cargo.exception.FlowerCargoNotFoundException; +import kr.bb.store.domain.cargo.exception.LockInterruptedException; +import kr.bb.store.domain.cargo.exception.StockCannotBeNegativeException; +import kr.bb.store.domain.cargo.exception.StockChangeFailedException; +import kr.bb.store.domain.cargo.repository.FlowerCargoRepository; +import kr.bb.store.domain.store.entity.Store; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.redisson.api.RLock; +import org.redisson.api.RedissonClient; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import static kr.bb.store.util.RedisUtils.makeRedissonKey; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CargoService { + private final RedissonClient redissonClient; + private final FlowerCargoRepository flowerCargoRepository; + private static final Long EMPTY_COUNT = 0L; + private static final Long STOCK_ALERT_COUNT = 50L; + + @Value("${redisson.lock.wait-second}") + private Integer waitSecond; + + @Value("${redisson.lock.lease-second}") + private Integer leaseSecond; + + @Transactional + public void modifyAllStocks(Long storeId, List stockModifyDtos) { + stockModifyDtos.forEach(stockModifyDto -> { + if(stockModifyDto.getStock() < EMPTY_COUNT) { + throw new StockCannotBeNegativeException(); + } + flowerCargoRepository.modifyStock(storeId, stockModifyDto.getFlowerId(), stockModifyDto.getStock()); + }); + } + + @Transactional + public Set plusStockCounts(List stockChangeDtos) { + return stockChangeDtos.stream() + .flatMap(stockChangeDto -> stockChangeDto.getStockDtos().stream() + .map(stockDto -> { + long storeId = stockChangeDto.getStoreId(); + long flowerId = stockDto.getFlowerId(); + long stockCount = stockDto.getStock(); + RLock lock = redissonClient.getLock(makeRedissonKey(storeId, flowerId)); + try { + boolean available = lock.tryLock(waitSecond, leaseSecond, TimeUnit.SECONDS); + if (!available) { + throw new StockChangeFailedException(); + } + + FlowerCargo flowerCargo = getFlowerCargo(storeId, flowerId); + + long afterPlusCount = flowerCargo.getStock() + stockCount; + if (isOutOfStock(afterPlusCount)) { + throw new StockCannotBeNegativeException(); + } + + flowerCargoRepository.plusStock(flowerCargo.getId().getStoreId(), flowerId, stockCount); + + if (isInsufficientCondition(afterPlusCount)) { + return storeId; + } + } catch (InterruptedException e) { + throw new LockInterruptedException(); + } finally { + unlock(lock); + } + return null; + }) + ) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + @Transactional + public Set minusStockCounts(List stockChangeDtos) { + return stockChangeDtos.stream() + .flatMap(stockChangeDto -> stockChangeDto.getStockDtos().stream() + .map(stockDto -> { + long storeId = stockChangeDto.getStoreId(); + long flowerId = stockDto.getFlowerId(); + long stockCount = stockDto.getStock(); + RLock lock = redissonClient.getLock(makeRedissonKey(storeId, flowerId)); + try { + boolean available = lock.tryLock(waitSecond, leaseSecond, TimeUnit.SECONDS); + if (!available) { + throw new StockChangeFailedException(); + } + + FlowerCargo flowerCargo = getFlowerCargo(stockChangeDto.getStoreId(), flowerId); + + long afterMinusCount = flowerCargo.getStock() - stockCount; + if (isOutOfStock(afterMinusCount)) { + throw new StockCannotBeNegativeException(); + } + + flowerCargoRepository.minusStock(storeId, flowerId, stockCount); + + if (isInsufficientCondition(afterMinusCount)) { + return storeId; + } + } catch (InterruptedException e) { + throw new LockInterruptedException(); + } finally { + unlock(lock); + } + return null; + })) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + } + + @Transactional + public void createBasicCargo(Store store, List flowers) { + List flowerCargos = flowers.stream() + .map(flowerDto -> FlowerCargo.builder() + .id(makeId(store.getId(), flowerDto.getFlowerId())) + .store(store) + .flowerName(flowerDto.getFlowerName()) + .build() + ) + .collect(Collectors.toList()); + flowerCargoRepository.saveAll(flowerCargos); + } + + public RemainingStocksResponse getAllStocks(Long storeId) { + List flowerCargos = flowerCargoRepository.findAllByStoreId(storeId); + List stockInfoDtos = flowerCargos.stream() + .map(StockInfoDto::fromEntity) + .collect(Collectors.toList()); + + return RemainingStocksResponse.from(stockInfoDtos); + } + + private FlowerCargo getFlowerCargo(Long storeId, Long flowerId) { + FlowerCargoId flowerCargoId = makeId(storeId, flowerId); + return flowerCargoRepository.findById(flowerCargoId) + .orElseThrow(FlowerCargoNotFoundException::new); + } + + private FlowerCargoId makeId(Long storeId, Long flowerId) { + return FlowerCargoId.builder() + .storeId(storeId) + .flowerId(flowerId) + .build(); + } + + + private boolean isOutOfStock(long afterChangeCount) { + return afterChangeCount < EMPTY_COUNT; + } + + private boolean isInsufficientCondition(Long stockCount) { + return stockCount < STOCK_ALERT_COUNT; + } + + private void unlock(RLock lock) { + if(lock.isLocked() && lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } +} diff --git a/src/main/java/kr/bb/store/domain/common/entity/BaseEntity.java b/src/main/java/kr/bb/store/domain/common/entity/BaseEntity.java new file mode 100644 index 0000000..ca39f78 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/common/entity/BaseEntity.java @@ -0,0 +1,31 @@ +package kr.bb.store.domain.common.entity; + +import lombok.Getter; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.LastModifiedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +import javax.persistence.Column; +import javax.persistence.EntityListeners; +import javax.persistence.MappedSuperclass; +import java.time.LocalDateTime; + +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseEntity { + @CreatedDate + @Column(name = "created_at") + private LocalDateTime createdAt; + + @LastModifiedDate + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + @Column(name = "is_deleted") + private Boolean isDeleted = false; + + public void softDelete() { + this.isDeleted = true; + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/CouponController.java b/src/main/java/kr/bb/store/domain/coupon/controller/CouponController.java new file mode 100644 index 0000000..d51cf6c --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/CouponController.java @@ -0,0 +1,82 @@ +package kr.bb.store.domain.coupon.controller; + +import bloomingblooms.response.CommonResponse; +import kr.bb.store.domain.coupon.controller.request.CouponCreateRequest; +import kr.bb.store.domain.coupon.controller.request.CouponEditRequest; +import kr.bb.store.domain.coupon.controller.request.TotalAmountRequest; +import kr.bb.store.domain.coupon.controller.request.UserInfoRequest; +import kr.bb.store.domain.coupon.controller.response.CouponIssuerResponse; +import kr.bb.store.domain.coupon.controller.response.CouponsForOwnerResponse; +import kr.bb.store.domain.coupon.controller.response.CouponsForUserResponse; +import kr.bb.store.domain.coupon.facade.CouponFacade; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +@RestController +@RequiredArgsConstructor +public class CouponController { + private final CouponFacade couponFacade; + + @PostMapping("/{storeId}/coupons") + public void createCoupon(@PathVariable Long storeId, + @RequestBody CouponCreateRequest couponCreateRequest) { + couponFacade.createCoupon(storeId, couponCreateRequest); + } + + @PutMapping("/{storeId}/coupons/{couponId}") + public void editCoupon(@PathVariable Long storeId, @PathVariable Long couponId, + @RequestBody CouponEditRequest couponEditRequest) { + couponFacade.editCoupon(storeId, couponId, couponEditRequest); + } + + @DeleteMapping("/{storeId}/coupons/{couponId}") + public void deleteCoupon(@PathVariable Long storeId, @PathVariable Long couponId) { + couponFacade.softDeleteCoupon(storeId, couponId); + } + + @GetMapping("/{storeId}/coupons") + public CommonResponse coupons(@PathVariable Long storeId) { + return CommonResponse.success(couponFacade.getAllStoreCoupons(storeId)); + } + + @PostMapping("/coupons/{couponId}") + public void downloadCoupon(@PathVariable Long couponId, + @RequestHeader(value = "userId") Long userId, + @RequestBody UserInfoRequest userInfoRequest) { + couponFacade.downloadCoupon(userId, couponId, userInfoRequest.getNickname(), userInfoRequest.getPhoneNumber(), LocalDate.now()); + } + + @PostMapping("/{storeId}/coupons/all") + public void downloadAllCoupons(@PathVariable Long storeId, + @RequestHeader(value = "userId") Long userId, + @RequestBody UserInfoRequest userInfoRequest) { + couponFacade.downloadAllCoupons(userId, storeId, userInfoRequest.getNickname(), userInfoRequest.getPhoneNumber(), LocalDate.now()); + } + + @GetMapping("/{storeId}/coupons/product") + public CommonResponse storeCouponsForUser(@PathVariable Long storeId, + @RequestHeader(value = "userId", required = false) Long userId) { + return CommonResponse.success(couponFacade.getAllStoreCouponsForUser(userId, storeId)); + } + + @PostMapping("/{storeId}/coupons/payment") + public CommonResponse couponsInPaymentStep(@PathVariable Long storeId, + @RequestHeader(value = "userId") Long userId, + @RequestBody TotalAmountRequest totalAmountRequest) { + return CommonResponse.success(couponFacade.getAvailableCouponsInPayment(totalAmountRequest, userId, storeId)); + } + + @GetMapping("/coupons/my") + public CommonResponse myCoupons(@RequestHeader(value = "userId") Long userId) { + return CommonResponse.success(couponFacade.getMyValidCoupons(userId)); + } + + @GetMapping("/coupons/{couponId}/members") + public CommonResponse couponMembers(@RequestHeader(value = "userId") Long userId, + @PathVariable Long couponId, Pageable pageable) { + return CommonResponse.success(couponFacade.getCouponMembers(userId, couponId, pageable)); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/CouponFeignController.java b/src/main/java/kr/bb/store/domain/coupon/controller/CouponFeignController.java new file mode 100644 index 0000000..528bb43 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/CouponFeignController.java @@ -0,0 +1,28 @@ +package kr.bb.store.domain.coupon.controller; + +import bloomingblooms.response.CommonResponse; +import kr.bb.store.domain.coupon.service.CouponService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +@CrossOrigin(origins = "*") +@RestController +@RequiredArgsConstructor +@RequestMapping(("/client/stores/coupons")) +public class CouponFeignController { + private final CouponService couponService; + + @PostMapping("/{couponId}/users/{userId}") + public void useCoupon(@PathVariable Long couponId, @PathVariable Long userId) { + LocalDate useDate = LocalDate.now(); + couponService.useCoupon(couponId, userId, useDate); + } + + @GetMapping("/count") + public CommonResponse availableCouponCountOfUser(@RequestHeader(value = "userId") Long userId) { + LocalDate now = LocalDate.now(); + return CommonResponse.success(couponService.getMyAvailableCouponCount(userId, now)); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/TestController.java b/src/main/java/kr/bb/store/domain/coupon/controller/TestController.java new file mode 100644 index 0000000..6a96473 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/TestController.java @@ -0,0 +1,22 @@ +package kr.bb.store.domain.coupon.controller; + +import kr.bb.store.domain.coupon.controller.request.UserInfoRequest; +import kr.bb.store.domain.coupon.facade.CouponFacade; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDate; + +@RestController +@RequiredArgsConstructor +public class TestController { + private final CouponFacade couponFacade; + + @PostMapping("/test/coupons/{couponId}") + public void downloadCoupon(@PathVariable Long couponId, + @RequestHeader(value = "userId") Long userId, + @RequestBody UserInfoRequest userInfoRequest) { + couponFacade.downloadCoupon(userId, couponId, userInfoRequest.getNickname(), userInfoRequest.getPhoneNumber(), LocalDate.now()); + } + +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/request/CouponCreateRequest.java b/src/main/java/kr/bb/store/domain/coupon/controller/request/CouponCreateRequest.java new file mode 100644 index 0000000..93da6aa --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/request/CouponCreateRequest.java @@ -0,0 +1,33 @@ +package kr.bb.store.domain.coupon.controller.request; + +import kr.bb.store.domain.coupon.handler.dto.CouponDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CouponCreateRequest { + private String couponName; + private Long discountPrice; + private Long minPrice; + private Integer limitCount; + private LocalDate startDate; + private LocalDate endDate; + + public CouponDto toDto() { + return CouponDto.builder() + .couponName(couponName) + .discountPrice(discountPrice) + .minPrice(minPrice) + .limitCount(limitCount) + .startDate(startDate) + .endDate(endDate) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/request/CouponEditRequest.java b/src/main/java/kr/bb/store/domain/coupon/controller/request/CouponEditRequest.java new file mode 100644 index 0000000..ce19a06 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/request/CouponEditRequest.java @@ -0,0 +1,33 @@ +package kr.bb.store.domain.coupon.controller.request; + +import kr.bb.store.domain.coupon.handler.dto.CouponDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CouponEditRequest { + private String couponName; + private Long discountPrice; + private Long minPrice; + private Integer limitCount; + private LocalDate startDate; + private LocalDate endDate; + + public CouponDto toDto() { + return CouponDto.builder() + .couponName(couponName) + .discountPrice(discountPrice) + .minPrice(minPrice) + .limitCount(limitCount) + .startDate(startDate) + .endDate(endDate) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/request/CouponUseRequest.java b/src/main/java/kr/bb/store/domain/coupon/controller/request/CouponUseRequest.java new file mode 100644 index 0000000..b1b7aaa --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/request/CouponUseRequest.java @@ -0,0 +1,15 @@ +package kr.bb.store.domain.coupon.controller.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CouponUseRequest { + private Long couponId; + private Long userId; +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/request/TotalAmountRequest.java b/src/main/java/kr/bb/store/domain/coupon/controller/request/TotalAmountRequest.java new file mode 100644 index 0000000..189c70f --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/request/TotalAmountRequest.java @@ -0,0 +1,14 @@ +package kr.bb.store.domain.coupon.controller.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class TotalAmountRequest { + private Long totalAmount; +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/request/UserInfoRequest.java b/src/main/java/kr/bb/store/domain/coupon/controller/request/UserInfoRequest.java new file mode 100644 index 0000000..b1203f8 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/request/UserInfoRequest.java @@ -0,0 +1,15 @@ +package kr.bb.store.domain.coupon.controller.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserInfoRequest { + private String nickname; + private String phoneNumber; +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/response/CouponIssuerResponse.java b/src/main/java/kr/bb/store/domain/coupon/controller/response/CouponIssuerResponse.java new file mode 100644 index 0000000..a30d889 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/response/CouponIssuerResponse.java @@ -0,0 +1,25 @@ +package kr.bb.store.domain.coupon.controller.response; + +import kr.bb.store.domain.coupon.dto.IssuedCouponDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CouponIssuerResponse { + private List data; + private Long totalCnt; + + public static CouponIssuerResponse of(List data, Long totalCnt) { + return CouponIssuerResponse.builder() + .data(data) + .totalCnt(totalCnt) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/response/CouponsForOwnerResponse.java b/src/main/java/kr/bb/store/domain/coupon/controller/response/CouponsForOwnerResponse.java new file mode 100644 index 0000000..e399c3d --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/response/CouponsForOwnerResponse.java @@ -0,0 +1,23 @@ +package kr.bb.store.domain.coupon.controller.response; + +import kr.bb.store.domain.coupon.dto.CouponForOwnerDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CouponsForOwnerResponse { + private List data; + + public static CouponsForOwnerResponse from(List couponForOwnerDtos) { + return CouponsForOwnerResponse.builder() + .data(couponForOwnerDtos) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/controller/response/CouponsForUserResponse.java b/src/main/java/kr/bb/store/domain/coupon/controller/response/CouponsForUserResponse.java new file mode 100644 index 0000000..9390121 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/controller/response/CouponsForUserResponse.java @@ -0,0 +1,23 @@ +package kr.bb.store.domain.coupon.controller.response; + +import kr.bb.store.domain.coupon.dto.CouponDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CouponsForUserResponse { + private List data; + + public static CouponsForUserResponse from(List couponDtos) { + return CouponsForUserResponse.builder() + .data(couponDtos) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/dto/CouponDto.java b/src/main/java/kr/bb/store/domain/coupon/dto/CouponDto.java new file mode 100644 index 0000000..2c11e0e --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/dto/CouponDto.java @@ -0,0 +1,30 @@ +package kr.bb.store.domain.coupon.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor +public class CouponDto { + private Long couponId; + private String couponName; + private String storeName; + private Long discountPrice; + private LocalDate endDate; + private Long minPrice; + + @QueryProjection + public CouponDto(Long couponId, String couponName, String storeName, Long discountPrice, LocalDate endDate, Long minPrice) { + this.couponId = couponId; + this.couponName = couponName; + this.storeName = storeName; + this.discountPrice = discountPrice; + this.endDate = endDate; + this.minPrice = minPrice; + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/dto/CouponForOwnerDto.java b/src/main/java/kr/bb/store/domain/coupon/dto/CouponForOwnerDto.java new file mode 100644 index 0000000..f355fd8 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/dto/CouponForOwnerDto.java @@ -0,0 +1,36 @@ +package kr.bb.store.domain.coupon.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor +public class CouponForOwnerDto { + private Long key; + private String couponCode; + private String couponName; + private Long minPrice; + private Long discountPrice; + private Integer limitCount; + private Integer unusedCount; + private LocalDate startDate; + private LocalDate endDate; + + @QueryProjection + public CouponForOwnerDto(Long key, String couponCode, String couponName, Long minPrice, Long discountPrice, Integer limitCount, Integer unusedCount, LocalDate startDate, LocalDate endDate) { + this.key = key; + this.couponCode = couponCode; + this.couponName = couponName; + this.minPrice = minPrice; + this.discountPrice = discountPrice; + this.limitCount = limitCount; + this.unusedCount = unusedCount; + this.startDate = startDate; + this.endDate = endDate; + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/dto/CouponWithAvailabilityDto.java b/src/main/java/kr/bb/store/domain/coupon/dto/CouponWithAvailabilityDto.java new file mode 100644 index 0000000..70f5f6c --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/dto/CouponWithAvailabilityDto.java @@ -0,0 +1,19 @@ +package kr.bb.store.domain.coupon.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor +public class CouponWithAvailabilityDto extends CouponDto { + private Boolean isAvailable; + + @QueryProjection + public CouponWithAvailabilityDto(Long couponId, String couponName, String storeName, Long discountPrice, LocalDate endDate, Long minPrice, Boolean isAvailable) { + super(couponId, couponName, storeName, discountPrice, endDate, minPrice); + this.isAvailable = isAvailable; + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/dto/CouponWithIssueStatusDto.java b/src/main/java/kr/bb/store/domain/coupon/dto/CouponWithIssueStatusDto.java new file mode 100644 index 0000000..d3908a4 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/dto/CouponWithIssueStatusDto.java @@ -0,0 +1,19 @@ +package kr.bb.store.domain.coupon.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor +public class CouponWithIssueStatusDto extends CouponDto { + private Boolean isIssued; + + @QueryProjection + public CouponWithIssueStatusDto(Long couponId, String couponName, String storeName, Long discountPrice, LocalDate endDate, Long minPrice, Boolean isIssued) { + super(couponId, couponName, storeName, discountPrice, endDate, minPrice); + this.isIssued = isIssued; + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/dto/IssuedCouponDto.java b/src/main/java/kr/bb/store/domain/coupon/dto/IssuedCouponDto.java new file mode 100644 index 0000000..c9be4da --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/dto/IssuedCouponDto.java @@ -0,0 +1,29 @@ +package kr.bb.store.domain.coupon.dto; + +import kr.bb.store.domain.coupon.entity.IssuedCoupon; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class IssuedCouponDto { + private String nickname; + private String phoneNumber; + private LocalDateTime createdAt; + private Boolean isUsed; + + public static IssuedCouponDto fromEntity(IssuedCoupon issuedCoupon) { + return IssuedCouponDto.builder() + .nickname(issuedCoupon.getNickname()) + .phoneNumber(issuedCoupon.getPhoneNumber()) + .createdAt(issuedCoupon.getCreatedAt()) + .isUsed(issuedCoupon.getIsUsed()) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/entity/Coupon.java b/src/main/java/kr/bb/store/domain/coupon/entity/Coupon.java new file mode 100644 index 0000000..1e192cb --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/entity/Coupon.java @@ -0,0 +1,88 @@ +package kr.bb.store.domain.coupon.entity; + +import kr.bb.store.domain.common.entity.BaseEntity; +import kr.bb.store.domain.coupon.exception.InvalidCouponDurationException; +import kr.bb.store.domain.coupon.exception.InvalidCouponStartDateException; +import kr.bb.store.domain.store.entity.Store; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import java.time.LocalDate; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class Coupon extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private String couponCode; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="store_id") + private Store store; + + @NotNull + private Integer limitCount; + + @NotNull + private String couponName; + + @NotNull + private Long discountPrice; + + @NotNull + private Long minPrice; + + @NotNull + private LocalDate startDate; + + @NotNull + private LocalDate endDate; + + @Builder + public Coupon(String couponCode, Store store, Integer limitCount, String couponName, Long discountPrice, Long minPrice, LocalDate startDate, LocalDate endDate) { + dateValidationCheck(startDate, endDate); + + this.couponCode = couponCode; + this.store = store; + this.limitCount = limitCount; + this.couponName = couponName; + this.discountPrice = discountPrice; + this.minPrice = minPrice; + this.startDate = startDate; + this.endDate = endDate; + } + + public void update(Integer limitCount, String couponName, Long discountPrice, Long minPrice, + LocalDate startDate, LocalDate endDate) { + dateValidationCheck(startDate, endDate); + + this.limitCount = limitCount; + this.couponName = couponName; + this.discountPrice = discountPrice; + this.minPrice = minPrice; + this.startDate = startDate; + this.endDate = endDate; + } + + public boolean isExpired(LocalDate now) { + if(this.endDate.isBefore(now)) return true; + return false; + } + + public boolean isRightPrice(long receivedPaymentPrice, long receivedDiscountPrice) { + return minPrice <= receivedPaymentPrice && discountPrice == receivedDiscountPrice; + } + + private void dateValidationCheck(LocalDate startDate, LocalDate endDate) { + if(startDate.isBefore(LocalDate.now())) throw new InvalidCouponStartDateException(); + if(endDate.isBefore(startDate)) throw new InvalidCouponDurationException(); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/entity/IssuedCoupon.java b/src/main/java/kr/bb/store/domain/coupon/entity/IssuedCoupon.java new file mode 100644 index 0000000..86c701b --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/entity/IssuedCoupon.java @@ -0,0 +1,45 @@ +package kr.bb.store.domain.coupon.entity; + + +import kr.bb.store.domain.common.entity.BaseEntity; +import kr.bb.store.domain.coupon.exception.AlreadyUsedCouponException; +import kr.bb.store.domain.coupon.exception.ExpiredCouponException; +import lombok.*; +import org.hibernate.annotations.DynamicInsert; + +import javax.persistence.*; +import java.time.LocalDate; + +@Entity +@Getter +@Builder +@DynamicInsert +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@AllArgsConstructor +public class IssuedCoupon extends BaseEntity { + @EmbeddedId + private IssuedCouponId id; + + @MapsId("couponId") + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="coupon_id") + private Coupon coupon; + + private String nickname; + + private String phoneNumber; + + @Builder.Default + @Column(nullable = false, columnDefinition = "boolean default false") + private Boolean isUsed = false; + + public void use(LocalDate now) { + if(isUsed) throw new AlreadyUsedCouponException(); + if(coupon.isExpired(now)) throw new ExpiredCouponException(); + isUsed = true; + } + + public void unUse() { + isUsed = false; + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/entity/IssuedCouponId.java b/src/main/java/kr/bb/store/domain/coupon/entity/IssuedCouponId.java new file mode 100644 index 0000000..e107d97 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/entity/IssuedCouponId.java @@ -0,0 +1,33 @@ +package kr.bb.store.domain.coupon.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Getter +@Builder +@Embeddable +@NoArgsConstructor +@AllArgsConstructor +public class IssuedCouponId implements Serializable { + private Long couponId; + private Long userId; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + IssuedCouponId that = (IssuedCouponId) o; + return Objects.equals(couponId, that.couponId) && Objects.equals(userId, that.userId); + } + + @Override + public int hashCode() { + return Objects.hash(couponId, userId); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/AlreadyIssuedCouponException.java b/src/main/java/kr/bb/store/domain/coupon/exception/AlreadyIssuedCouponException.java new file mode 100644 index 0000000..7b6b46a --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/AlreadyIssuedCouponException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.coupon.exception; + +import kr.bb.store.exception.CustomException; + +public class AlreadyIssuedCouponException extends CustomException { + private static final String MESSAGE = "์ด๋ฏธ ๋ฐœ๊ธ‰๋ฐ›์€ ์ฟ ํฐ์ž…๋‹ˆ๋‹ค."; + + public AlreadyIssuedCouponException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/AlreadyUsedCouponException.java b/src/main/java/kr/bb/store/domain/coupon/exception/AlreadyUsedCouponException.java new file mode 100644 index 0000000..b44aa65 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/AlreadyUsedCouponException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.coupon.exception; + +import kr.bb.store.exception.CustomException; + +public class AlreadyUsedCouponException extends CustomException { + private static final String MESSAGE = "์ด๋ฏธ ์‚ฌ์šฉํ•œ ์ฟ ํฐ์ž…๋‹ˆ๋‹ค."; + + public AlreadyUsedCouponException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/CouponInconsistencyException.java b/src/main/java/kr/bb/store/domain/coupon/exception/CouponInconsistencyException.java new file mode 100644 index 0000000..40e181a --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/CouponInconsistencyException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.coupon.exception; + +import kr.bb.store.exception.CustomException; + +public class CouponInconsistencyException extends CustomException { + private static final String MESSAGE = "ํ•ด๋‹น ์š”์ฒญ์€ ์‹ค์ œ ์ฟ ํฐ ์ •๋ณด์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + + public CouponInconsistencyException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/CouponNotFoundException.java b/src/main/java/kr/bb/store/domain/coupon/exception/CouponNotFoundException.java new file mode 100644 index 0000000..5624b68 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/CouponNotFoundException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.coupon.exception; + +import kr.bb.store.exception.CustomException; + +public class CouponNotFoundException extends CustomException { + private static final String MESSAGE = "ํ•ด๋‹น ์ฟ ํฐ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + + public CouponNotFoundException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/CouponOutOfStockException.java b/src/main/java/kr/bb/store/domain/coupon/exception/CouponOutOfStockException.java new file mode 100644 index 0000000..208dbea --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/CouponOutOfStockException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.coupon.exception; + +import kr.bb.store.exception.CustomException; + +public class CouponOutOfStockException extends CustomException { + private static final String MESSAGE = "์ค€๋น„๋œ ์ฟ ํฐ์ด ๋ชจ๋‘ ์†Œ์ง„๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; + + public CouponOutOfStockException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/DeletedCouponException.java b/src/main/java/kr/bb/store/domain/coupon/exception/DeletedCouponException.java new file mode 100644 index 0000000..bbb3f64 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/DeletedCouponException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.coupon.exception; + +import kr.bb.store.exception.CustomException; + +public class DeletedCouponException extends CustomException { + private final static String MESSAGE = "ํ•ด๋‹น ์ฟ ํฐ์€ ๊ด€๋ฆฌ์ž์— ์˜ํ•ด ์‚ญ์ œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."; + + public DeletedCouponException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/ExpiredCouponException.java b/src/main/java/kr/bb/store/domain/coupon/exception/ExpiredCouponException.java new file mode 100644 index 0000000..ee4bd8a --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/ExpiredCouponException.java @@ -0,0 +1,9 @@ +package kr.bb.store.domain.coupon.exception; + +public class ExpiredCouponException extends RuntimeException { + private static final String MESSAGE = "๊ธฐํ•œ์ด ๋งŒ๋ฃŒ๋œ ์ฟ ํฐ์ž…๋‹ˆ๋‹ค."; + + public ExpiredCouponException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/InvalidCouponDurationException.java b/src/main/java/kr/bb/store/domain/coupon/exception/InvalidCouponDurationException.java new file mode 100644 index 0000000..277421b --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/InvalidCouponDurationException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.coupon.exception; + +import kr.bb.store.exception.CustomException; + +public class InvalidCouponDurationException extends CustomException { + private static final String MESSAGE = "์‹œ์ž‘์ผ๊ณผ ์ข…๋ฃŒ์ผ์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + + public InvalidCouponDurationException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/InvalidCouponStartDateException.java b/src/main/java/kr/bb/store/domain/coupon/exception/InvalidCouponStartDateException.java new file mode 100644 index 0000000..60d9a8f --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/InvalidCouponStartDateException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.coupon.exception; + +import kr.bb.store.exception.CustomException; + +public class InvalidCouponStartDateException extends CustomException { + private static final String MESSAGE = "์‹œ์ž‘์ผ์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + + public InvalidCouponStartDateException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/NotIssuedCouponException.java b/src/main/java/kr/bb/store/domain/coupon/exception/NotIssuedCouponException.java new file mode 100644 index 0000000..3dab4aa --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/NotIssuedCouponException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.coupon.exception; + +import kr.bb.store.exception.CustomException; + +public class NotIssuedCouponException extends CustomException { + private static final String MESSAGE = "๋ฐœ๊ธ‰๋œ์  ์—†๋Š” ์ฟ ํฐ์ž…๋‹ˆ๋‹ค."; + + public NotIssuedCouponException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/exception/UnAuthorizedCouponException.java b/src/main/java/kr/bb/store/domain/coupon/exception/UnAuthorizedCouponException.java new file mode 100644 index 0000000..ed89398 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/exception/UnAuthorizedCouponException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.coupon.exception; + +import kr.bb.store.exception.CustomException; + +public class UnAuthorizedCouponException extends CustomException { + private static final String MESSAGE = "ํ•ด๋‹น ์ฟ ํฐ์— ๋Œ€ํ•œ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."; + + public UnAuthorizedCouponException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/facade/CouponFacade.java b/src/main/java/kr/bb/store/domain/coupon/facade/CouponFacade.java new file mode 100644 index 0000000..8e01671 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/facade/CouponFacade.java @@ -0,0 +1,96 @@ +package kr.bb.store.domain.coupon.facade; + +import bloomingblooms.domain.notification.NotificationKind; +import bloomingblooms.domain.order.ProcessOrderDto; +import kr.bb.store.domain.coupon.controller.request.CouponCreateRequest; +import kr.bb.store.domain.coupon.controller.request.CouponEditRequest; +import kr.bb.store.domain.coupon.controller.request.TotalAmountRequest; +import kr.bb.store.domain.coupon.controller.response.CouponIssuerResponse; +import kr.bb.store.domain.coupon.controller.response.CouponsForOwnerResponse; +import kr.bb.store.domain.coupon.controller.response.CouponsForUserResponse; +import kr.bb.store.domain.coupon.service.CouponService; +import kr.bb.store.message.OrderStatusSQSPublisher; +import kr.bb.store.util.KafkaProcessor; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CouponFacade { + private final CouponService couponService; + private final OrderStatusSQSPublisher orderStatusSQSPublisher; + private final KafkaProcessor stockDecreaseKafkaProducer; + + @KafkaListener(topics = "coupon-use", groupId = "use-coupon") + public void useCoupons(ProcessOrderDto processOrderDto) { + try { + LocalDate useDate = LocalDate.now(); + couponService.useAllCoupons(processOrderDto.getCouponIds(), processOrderDto.getUserId(), useDate); + log.info("coupon from order {} used successfully", processOrderDto.getOrderId()); + stockDecreaseKafkaProducer.send("stock-decrease", processOrderDto); + } catch (Exception e) { + Long userId = processOrderDto.getUserId(); + String phoneNumber = processOrderDto.getPhoneNumber(); + orderStatusSQSPublisher.publish(userId, phoneNumber, NotificationKind.INVALID_COUPON); + log.error("coupon use failed with cause of {}", e); + } + } + + @KafkaListener(topics = "stock-decrease-rollback", groupId = "rollback-coupon") + public void rollbackCoupons(ProcessOrderDto processOrderDto) { + couponService.unUseAllCoupons(processOrderDto.getCouponIds(), processOrderDto.getUserId()); + log.info("coupon from order {} rollbacked successfully", processOrderDto.getOrderId()); + } + + public void createCoupon(Long storeId, CouponCreateRequest couponCreateRequest) { + couponService.createCoupon(storeId, couponCreateRequest); + } + + public void editCoupon(Long storeId, Long couponId, CouponEditRequest couponEditRequest) { + couponService.editCoupon(storeId, couponId, couponEditRequest); + } + + public void softDeleteCoupon(Long storeId, Long couponId) { + couponService.softDeleteCoupon(storeId, couponId); + } + + public CouponsForOwnerResponse getAllStoreCoupons(Long storeId) { + LocalDate now = LocalDate.now(); + return CouponsForOwnerResponse.from(couponService.getAllStoreCoupons(storeId, now)); + } + + public void downloadCoupon(Long userId, Long couponId, String nickname, String phoneNumber, LocalDate now) { + couponService.downloadCoupon(userId, couponId, nickname, phoneNumber, now); + } + + public void downloadAllCoupons(Long userId, Long storeId, String nickname, String phoneNumber, LocalDate now) { + couponService.downloadAllCoupons(userId, storeId, nickname, phoneNumber, now); + } + + public CouponsForUserResponse getAllStoreCouponsForUser(Long userId, Long storeId) { + LocalDate now = LocalDate.now(); + return CouponsForUserResponse.from(couponService.getAllStoreCouponsForUser(userId, storeId, now)); + } + + public CouponsForUserResponse getAvailableCouponsInPayment(TotalAmountRequest totalAmountRequest, + Long userId, Long storeId) { + LocalDate now = LocalDate.now(); + return CouponsForUserResponse.from(couponService.getAvailableCouponsInPayment(totalAmountRequest, userId, storeId, now)); + } + + public CouponsForUserResponse getMyValidCoupons(Long userId) { + LocalDate now = LocalDate.now(); + return CouponsForUserResponse.from(couponService.getMyValidCoupons(userId, now)); + } + + public CouponIssuerResponse getCouponMembers(Long userId, Long couponId, Pageable pageable) { + return couponService.getCouponMembers(userId, couponId, pageable); + } + +} \ No newline at end of file diff --git a/src/main/java/kr/bb/store/domain/coupon/handler/CouponCreator.java b/src/main/java/kr/bb/store/domain/coupon/handler/CouponCreator.java new file mode 100644 index 0000000..a42065c --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/handler/CouponCreator.java @@ -0,0 +1,34 @@ +package kr.bb.store.domain.coupon.handler; + +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.handler.dto.CouponDto; +import kr.bb.store.domain.coupon.repository.CouponRepository; +import kr.bb.store.domain.store.entity.Store; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@RequiredArgsConstructor +@Component +public class CouponCreator { + private final CouponRepository couponRepository; + + public Coupon create(Store store, CouponDto couponDto) { + + Coupon coupon = Coupon.builder() + .couponCode(UUID.randomUUID().toString().substring(0,8)) + .store(store) + .limitCount(couponDto.getLimitCount()) + .couponName(couponDto.getCouponName()) + .discountPrice(couponDto.getDiscountPrice()) + .minPrice(couponDto.getMinPrice()) + .startDate(couponDto.getStartDate()) + .endDate(couponDto.getEndDate()) + .build(); + + return couponRepository.save(coupon); + + } + +} diff --git a/src/main/java/kr/bb/store/domain/coupon/handler/CouponIssuer.java b/src/main/java/kr/bb/store/domain/coupon/handler/CouponIssuer.java new file mode 100644 index 0000000..1d7309e --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/handler/CouponIssuer.java @@ -0,0 +1,89 @@ +package kr.bb.store.domain.coupon.handler; + +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.entity.IssuedCoupon; +import kr.bb.store.domain.coupon.entity.IssuedCouponId; +import kr.bb.store.domain.coupon.exception.AlreadyIssuedCouponException; +import kr.bb.store.domain.coupon.exception.CouponOutOfStockException; +import kr.bb.store.domain.coupon.exception.DeletedCouponException; +import kr.bb.store.domain.coupon.exception.ExpiredCouponException; +import kr.bb.store.domain.coupon.repository.IssuedCouponRepository; +import kr.bb.store.util.RedisOperation; +import kr.bb.store.util.luascript.CouponLockExecutor; +import kr.bb.store.util.luascript.RedisLuaScriptExecutor; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; +import java.util.function.Predicate; + +import static kr.bb.store.util.RedisUtils.makeRedisKey; +import static kr.bb.store.util.luascript.LockScript.script; + +@Component +public class CouponIssuer { + private final IssuedCouponRepository issuedCouponRepository; + private final RedisLuaScriptExecutor redisLuaScriptExecutor; + private final RedisOperation redisOperation; + + public CouponIssuer(IssuedCouponRepository issuedCouponRepository, CouponLockExecutor couponLockExecutor, RedisOperation redisOperation) { + this.issuedCouponRepository = issuedCouponRepository; + this.redisLuaScriptExecutor = couponLockExecutor; + this.redisOperation = redisOperation; + } + + public IssuedCoupon issueCoupon(Coupon coupon, Long userId, String nickname, String phoneNumber, LocalDate issueDate) { + if(coupon.getIsDeleted()) throw new DeletedCouponException(); + if(coupon.isExpired(issueDate)) throw new ExpiredCouponException(); + + String redisKey = makeRedisKey(coupon); + String redisValue = userId.toString(); + Integer limitCnt = coupon.getLimitCount(); + if(isDuplicated(redisKey, redisValue)) throw new AlreadyIssuedCouponException(); + + boolean issuable = (Boolean)redisLuaScriptExecutor.execute(script, redisKey, redisValue, limitCnt); + if(issuable) { + return issuedCouponRepository.save(makeIssuedCoupon(coupon,userId,nickname,phoneNumber)); + } + throw new CouponOutOfStockException(); + } + + public void issuePossibleCoupons(List coupons, Long userId, String nickname, String phoneNumber, LocalDate issueDate) { + final String redisValue = userId.toString(); + + coupons.stream() + .filter(Predicate.not(Coupon::getIsDeleted)) + .filter(Predicate.not(coupon -> coupon.isExpired(issueDate))) + .filter(Predicate.not(coupon -> { + String redisKey = makeRedisKey(coupon); + return isDuplicated(redisKey,redisValue); + })) + .filter(coupon -> { + String redisKey = makeRedisKey(coupon); + Integer limitCnt = coupon.getLimitCount(); + return (Boolean)redisLuaScriptExecutor.execute(script, redisKey, redisValue, limitCnt); + }) + .forEach(coupon -> issuedCouponRepository.save(makeIssuedCoupon(coupon,userId,nickname,phoneNumber))); + } + + private IssuedCoupon makeIssuedCoupon(Coupon coupon, Long userId, String nickname, String phoneNumber) { + return IssuedCoupon.builder() + .id(makeIssuedCouponId(coupon.getId(), userId)) + .nickname(nickname) + .phoneNumber(phoneNumber) + .coupon(coupon) + .build(); + } + + private IssuedCouponId makeIssuedCouponId(Long couponId, Long userId) { + return IssuedCouponId.builder() + .couponId(couponId) + .userId(userId) + .build(); + } + + private boolean isDuplicated(String redisKey, String value) { + return redisOperation.contains(redisKey, value); + } + +} diff --git a/src/main/java/kr/bb/store/domain/coupon/handler/CouponManager.java b/src/main/java/kr/bb/store/domain/coupon/handler/CouponManager.java new file mode 100644 index 0000000..c62384c --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/handler/CouponManager.java @@ -0,0 +1,39 @@ +package kr.bb.store.domain.coupon.handler; + +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.entity.IssuedCoupon; +import kr.bb.store.domain.coupon.handler.dto.CouponDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; + +@Component +@RequiredArgsConstructor +public class CouponManager { + + public void edit(Coupon coupon, CouponDto couponEditDto) { + coupon.update( + couponEditDto.getLimitCount(), + couponEditDto.getCouponName(), + couponEditDto.getDiscountPrice(), + couponEditDto.getMinPrice(), + couponEditDto.getStartDate(), + couponEditDto.getEndDate() + ); + } + + public void use(IssuedCoupon issuedCoupon, LocalDate useDate) { + issuedCoupon.use(useDate); + } + + public void unUse(IssuedCoupon issuedCoupon) { + issuedCoupon.unUse(); + } + + public void softDelete(Coupon coupon) { + coupon.softDelete(); + } + + +} diff --git a/src/main/java/kr/bb/store/domain/coupon/handler/CouponReader.java b/src/main/java/kr/bb/store/domain/coupon/handler/CouponReader.java new file mode 100644 index 0000000..ef78cb0 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/handler/CouponReader.java @@ -0,0 +1,49 @@ +package kr.bb.store.domain.coupon.handler; + +import kr.bb.store.domain.coupon.dto.CouponDto; +import kr.bb.store.domain.coupon.dto.CouponForOwnerDto; +import kr.bb.store.domain.coupon.dto.CouponWithAvailabilityDto; +import kr.bb.store.domain.coupon.dto.CouponWithIssueStatusDto; +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.exception.CouponNotFoundException; +import kr.bb.store.domain.coupon.repository.CouponRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; +import java.util.List; + +@Component +@RequiredArgsConstructor +public class CouponReader { + private final CouponRepository couponRepository; + + public Coupon read(Long couponId) { + return couponRepository.findById(couponId).orElseThrow(CouponNotFoundException::new); + } + + public List readCouponsForOwner(Long storeId, LocalDate now) { + return couponRepository.findAllDtoByStoreId(storeId, now); + } + + public List readStoresAllValidateCoupon(Long storeId, LocalDate now) { + return couponRepository.findAllValidateCouponsByStoreId(storeId, now); + } + + public List readStoreCouponsForUser(Long userId, Long storeId, LocalDate now) { + return couponRepository.findStoreCouponsForUser(userId, storeId, now); + } + + public List readAvailableCouponsInStore(Long totalAmount, Long userId, Long storeId, + LocalDate readDate) { + return couponRepository.findAvailableCoupons(totalAmount, userId, storeId, readDate); + } + + public List readMyValidCoupons(Long userId, LocalDate readDate) { + return couponRepository.findMyValidCoupons(userId, readDate); + } + + public Integer readMyValidCouponCount(Long userId, LocalDate readDate) { + return couponRepository.findMyValidCouponCount(userId, readDate); + } +} diff --git a/src/main/java/kr/bb/store/domain/coupon/handler/IssuedCouponReader.java b/src/main/java/kr/bb/store/domain/coupon/handler/IssuedCouponReader.java new file mode 100644 index 0000000..982bcf1 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/handler/IssuedCouponReader.java @@ -0,0 +1,40 @@ +package kr.bb.store.domain.coupon.handler; + +import kr.bb.store.domain.coupon.entity.IssuedCoupon; +import kr.bb.store.domain.coupon.entity.IssuedCouponId; +import kr.bb.store.domain.coupon.exception.NotIssuedCouponException; +import kr.bb.store.domain.coupon.repository.IssuedCouponRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; + +@RequiredArgsConstructor +@Component +public class IssuedCouponReader { + private final IssuedCouponRepository issuedCouponRepository; + + public IssuedCoupon read(Long couponId, Long userId) { + IssuedCouponId id = makeIssuedCouponId(couponId, userId); + return issuedCouponRepository.findById(id).orElseThrow(NotIssuedCouponException::new); + } + + public List readByCouponId(Long couponId, Pageable pageable) { + long offset = pageable.getOffset(); + int pageSize = pageable.getPageSize(); + return issuedCouponRepository.findByCouponId(couponId, offset, pageSize); + } + + public long countByCouponId(Long couponId) { + return issuedCouponRepository.countByCouponId(couponId); + } + + private IssuedCouponId makeIssuedCouponId(Long couponId, Long userId) { + return IssuedCouponId.builder() + .couponId(couponId) + .userId(userId) + .build(); + } + +} diff --git a/src/main/java/kr/bb/store/domain/coupon/handler/dto/CouponDto.java b/src/main/java/kr/bb/store/domain/coupon/handler/dto/CouponDto.java new file mode 100644 index 0000000..5c0ebb5 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/handler/dto/CouponDto.java @@ -0,0 +1,21 @@ +package kr.bb.store.domain.coupon.handler.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class CouponDto { + private String couponName; + private Long discountPrice; + private Long minPrice; + private Integer limitCount; + private LocalDate startDate; + private LocalDate endDate; +} diff --git a/src/main/java/kr/bb/store/domain/coupon/repository/CouponRepository.java b/src/main/java/kr/bb/store/domain/coupon/repository/CouponRepository.java new file mode 100644 index 0000000..1966475 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/repository/CouponRepository.java @@ -0,0 +1,7 @@ +package kr.bb.store.domain.coupon.repository; + +import kr.bb.store.domain.coupon.entity.Coupon; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface CouponRepository extends JpaRepository,CouponRepositoryCustom { +} diff --git a/src/main/java/kr/bb/store/domain/coupon/repository/CouponRepositoryCustom.java b/src/main/java/kr/bb/store/domain/coupon/repository/CouponRepositoryCustom.java new file mode 100644 index 0000000..aa03670 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/repository/CouponRepositoryCustom.java @@ -0,0 +1,19 @@ +package kr.bb.store.domain.coupon.repository; + +import kr.bb.store.domain.coupon.dto.CouponDto; +import kr.bb.store.domain.coupon.dto.CouponForOwnerDto; +import kr.bb.store.domain.coupon.dto.CouponWithAvailabilityDto; +import kr.bb.store.domain.coupon.dto.CouponWithIssueStatusDto; +import kr.bb.store.domain.coupon.entity.Coupon; + +import java.time.LocalDate; +import java.util.List; + +public interface CouponRepositoryCustom { + List findAllDtoByStoreId(Long storeId, LocalDate now); + List findAllValidateCouponsByStoreId(Long storeId, LocalDate now); + List findStoreCouponsForUser(Long userId, Long storeId, LocalDate now); + List findAvailableCoupons(Long totalAmount, Long userId, Long storeId, LocalDate now); + List findMyValidCoupons(Long userId, LocalDate now); + Integer findMyValidCouponCount(Long userId, LocalDate now); +} diff --git a/src/main/java/kr/bb/store/domain/coupon/repository/CouponRepositoryCustomImpl.java b/src/main/java/kr/bb/store/domain/coupon/repository/CouponRepositoryCustomImpl.java new file mode 100644 index 0000000..db74115 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/repository/CouponRepositoryCustomImpl.java @@ -0,0 +1,158 @@ +package kr.bb.store.domain.coupon.repository; + +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.JPAExpressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import kr.bb.store.domain.coupon.dto.*; +import kr.bb.store.domain.coupon.entity.Coupon; + +import javax.persistence.EntityManager; +import java.time.LocalDate; +import java.util.List; + +import static kr.bb.store.domain.coupon.entity.QCoupon.coupon; +import static kr.bb.store.domain.coupon.entity.QIssuedCoupon.issuedCoupon; + +public class CouponRepositoryCustomImpl implements CouponRepositoryCustom{ + private final JPAQueryFactory queryFactory; + + public CouponRepositoryCustomImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public List findAllDtoByStoreId(Long storeId, LocalDate now) { + return queryFactory + .select(new QCouponForOwnerDto( + coupon.id, + coupon.couponCode, + coupon.couponName, + coupon.minPrice, + coupon.discountPrice, + coupon.limitCount, + coupon.limitCount.subtract( + JPAExpressions + .select(issuedCoupon.count()) + .from(issuedCoupon) + .where(issuedCoupon.id.couponId.eq(coupon.id)) + ), + coupon.startDate, + coupon.endDate + )) + .from(coupon) + .where( + coupon.store.id.eq(storeId), + coupon.isDeleted.isFalse(), + coupon.endDate.goe(now) + ) + .fetch(); + } + + @Override + public List findAllValidateCouponsByStoreId(Long storeId, LocalDate now) { + return queryFactory + .selectFrom(coupon) + .where( + coupon.store.id.eq(storeId), + isCouponUnexpired(now), + coupon.isDeleted.isFalse() + ) + .fetch(); + } + + @Override + public List findStoreCouponsForUser(Long userId, Long storeId, LocalDate now) { + return queryFactory + .select(new QCouponWithIssueStatusDto( + coupon.id, + coupon.couponName, + coupon.store.storeName, + coupon.discountPrice, + coupon.endDate, + coupon.minPrice, + JPAExpressions + .select(issuedCoupon.count().gt(0)) + .from(issuedCoupon) + .where( + issuedCoupon.id.couponId.eq(coupon.id), + issuedCoupon.id.userId.eq(userId), + coupon.isDeleted.isFalse() + ) + )) + .from(coupon) + .where( + coupon.store.id.eq(storeId), + isCouponUnexpired(now), + coupon.isDeleted.isFalse() + ) + .fetch(); + } + + @Override + public List findAvailableCoupons(Long totalAmount, Long userId, Long storeId, LocalDate now) { + return queryFactory.select(new QCouponWithAvailabilityDto( + coupon.id, + coupon.couponName, + coupon.store.storeName, + coupon.discountPrice, + coupon.endDate, + coupon.minPrice, + coupon.minPrice.loe(totalAmount) + )) + .from(coupon) + .leftJoin(issuedCoupon) + .on(coupon.id.eq(issuedCoupon.id.couponId)) + .where( + coupon.store.id.eq(storeId), + issuedCoupon.id.userId.eq(userId), + issuedCoupon.isUsed.isFalse(), + isCouponUnexpired(now), + coupon.isDeleted.isFalse() + ) + .fetch(); + } + + @Override + public List findMyValidCoupons(Long userId, LocalDate now) { + return queryFactory + .select(new QCouponDto( + coupon.id, + coupon.couponName, + coupon.store.storeName, + coupon.discountPrice, + coupon.endDate, + coupon.minPrice + )) + .from(coupon) + .leftJoin(issuedCoupon) + .on(coupon.id.eq(issuedCoupon.id.couponId)) + .where( + issuedCoupon.id.userId.eq(userId), + issuedCoupon.isUsed.isFalse(), + isCouponUnexpired(now), + coupon.isDeleted.isFalse() + ) + .fetch(); + } + + @Override + public Integer findMyValidCouponCount(Long userId, LocalDate now) { + return Math.toIntExact(queryFactory + .select(coupon.count()) + .from(coupon) + .leftJoin(issuedCoupon) + .on(coupon.id.eq(issuedCoupon.id.couponId)) + .where( + issuedCoupon.id.userId.eq(userId), + issuedCoupon.isUsed.isFalse(), + isCouponUnexpired(now), + coupon.isDeleted.isFalse() + ) + .fetchFirst()); + } + + private BooleanExpression isCouponUnexpired(LocalDate now) { + return coupon.endDate.after(now).or(coupon.endDate.eq(now)); + } + +} diff --git a/src/main/java/kr/bb/store/domain/coupon/repository/IssuedCouponRepository.java b/src/main/java/kr/bb/store/domain/coupon/repository/IssuedCouponRepository.java new file mode 100644 index 0000000..62894ca --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/repository/IssuedCouponRepository.java @@ -0,0 +1,21 @@ +package kr.bb.store.domain.coupon.repository; + +import kr.bb.store.domain.coupon.entity.IssuedCoupon; +import kr.bb.store.domain.coupon.entity.IssuedCouponId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface IssuedCouponRepository extends JpaRepository { + @Query(value = "select * from issued_coupon where user_id = :userId and is_used = false",nativeQuery = true) + List findUsableCouponsByUserId(@Param("userId") Long userId); + + @Query(value = "SELECT * FROM issued_coupon WHERE coupon_id = :couponId ORDER BY created_at LIMIT :pageSize OFFSET :offset", nativeQuery = true) + List findByCouponId(@Param("couponId") Long couponId, @Param("offset") long offset, @Param("pageSize") int pageSize); + + long countByCouponId(Long couponId); +} diff --git a/src/main/java/kr/bb/store/domain/coupon/service/CouponService.java b/src/main/java/kr/bb/store/domain/coupon/service/CouponService.java new file mode 100644 index 0000000..5f59141 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/coupon/service/CouponService.java @@ -0,0 +1,152 @@ +package kr.bb.store.domain.coupon.service; + +import bloomingblooms.domain.order.ValidatePriceDto; +import kr.bb.store.domain.coupon.controller.request.CouponCreateRequest; +import kr.bb.store.domain.coupon.controller.request.CouponEditRequest; +import kr.bb.store.domain.coupon.controller.request.TotalAmountRequest; +import kr.bb.store.domain.coupon.controller.response.CouponIssuerResponse; +import kr.bb.store.domain.coupon.dto.*; +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.entity.IssuedCoupon; +import kr.bb.store.domain.coupon.exception.CouponInconsistencyException; +import kr.bb.store.domain.coupon.exception.UnAuthorizedCouponException; +import kr.bb.store.domain.coupon.handler.*; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.handler.StoreReader; +import kr.bb.store.util.RedisOperation; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import static kr.bb.store.util.RedisUtils.makeRedisKey; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class CouponService { + private final CouponCreator couponCreator; + private final CouponManager couponManager; + private final CouponReader couponReader; + private final IssuedCouponReader issuedCouponReader; + private final CouponIssuer couponIssuer; + private final StoreReader storeReader; + private final RedisOperation redisOperation; + + @Transactional + public void createCoupon(Long storeId, CouponCreateRequest couponCreateRequest) { + Store store = storeReader.findStoreById(storeId); + Coupon coupon = couponCreator.create(store, couponCreateRequest.toDto()); + + String redisKey = makeRedisKey(coupon); + redisOperation.addAndSetExpr(redisKey, coupon.getEndDate().plusDays(1)); + } + + @Transactional + public void editCoupon(Long storeId, Long couponId, CouponEditRequest couponEditRequest) { + Coupon coupon = couponReader.read(couponId); + validateCouponAuthorization(coupon,storeId); + couponManager.edit(coupon, couponEditRequest.toDto()); + + String redisKey = makeRedisKey(coupon); + redisOperation.setExpr(redisKey, couponEditRequest.getEndDate()); + } + + @Transactional + public void softDeleteCoupon(Long storeId, Long couponId) { + Coupon coupon = couponReader.read(couponId); + validateCouponAuthorization(coupon,storeId); + couponManager.softDelete(coupon); + } + + @Transactional + public void downloadCoupon(Long userId, Long couponId, String nickname, String phoneNumber, LocalDate now) { + Coupon coupon = couponReader.read(couponId); + couponIssuer.issueCoupon(coupon, userId, nickname, phoneNumber, now); + } + + @Transactional + public void downloadAllCoupons(Long userId, Long storeId, String nickname, String phoneNumber, LocalDate now) { + List coupons = couponReader.readStoresAllValidateCoupon(storeId, now); + couponIssuer.issuePossibleCoupons(coupons, userId, nickname, phoneNumber, now); + } + + @Transactional + public void useCoupon(Long couponId, Long userId, LocalDate useDate) { + IssuedCoupon issuedCoupon = issuedCouponReader.read(couponId,userId); + couponManager.use(issuedCoupon, useDate); + } + + @Transactional + public void useAllCoupons(List couponIds, Long userId, LocalDate useDate) { + couponIds.forEach(couponId -> { + IssuedCoupon issuedCoupon = issuedCouponReader.read(couponId,userId); + couponManager.use(issuedCoupon, useDate); + }); + } + + @Transactional + public void unUseAllCoupons(List couponIds, Long userId) { + couponIds.forEach(couponId -> { + IssuedCoupon issuedCoupon = issuedCouponReader.read(couponId,userId); + couponManager.unUse(issuedCoupon); + }); + } + + public List getAllStoreCoupons(Long storeId, LocalDate now) { + return couponReader.readCouponsForOwner(storeId, now); + } + + public List getAllStoreCouponsForUser(Long userId, Long storeId, LocalDate now) { + return couponReader.readStoreCouponsForUser(userId, storeId, now); + } + + public List getAvailableCouponsInPayment(TotalAmountRequest totalAmountRequest, + Long userId, Long storeId, LocalDate now) { + return couponReader.readAvailableCouponsInStore(totalAmountRequest.getTotalAmount(), userId, storeId, now); + } + + public List getMyValidCoupons(Long userId, LocalDate now) { + return couponReader.readMyValidCoupons(userId, now); + } + + public CouponIssuerResponse getCouponMembers(Long userId, Long couponId, Pageable pageable) { + Coupon coupon = couponReader.read(couponId); + if(!coupon.getStore().getStoreManagerId().equals(userId)) { + throw new UnAuthorizedCouponException(); + } + + List issuedCoupons = issuedCouponReader.readByCouponId(couponId, pageable) + .stream().map(IssuedCouponDto::fromEntity).collect(Collectors.toList()); + long count = issuedCouponReader.countByCouponId(couponId); + + return CouponIssuerResponse.of(issuedCoupons, count); + } + + public Integer getMyAvailableCouponCount(Long userId, LocalDate now) { + return couponReader.readMyValidCouponCount(userId, now); + } + + public void validateCouponPrice(List validatePriceDtos) { + validatePriceDtos.stream() + .filter(dto -> Objects.nonNull(dto.getCouponId())) + .forEach(dto -> { + Coupon coupon = couponReader.read(dto.getCouponId()); + Long receivedPaymentPrice = dto.getActualAmount(); + Long receivedDiscountPrice = dto.getCouponAmount(); + if(!coupon.isRightPrice(receivedPaymentPrice, receivedDiscountPrice)) { + throw new CouponInconsistencyException(); + } + }); + } + + private void validateCouponAuthorization(Coupon coupon, Long storeId) { + if(!coupon.getStore().getId().equals(storeId)) throw new UnAuthorizedCouponException(); + } + +} diff --git a/src/main/java/kr/bb/store/domain/question/controller/QuestionController.java b/src/main/java/kr/bb/store/domain/question/controller/QuestionController.java new file mode 100644 index 0000000..ce9d284 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/controller/QuestionController.java @@ -0,0 +1,61 @@ +package kr.bb.store.domain.question.controller; + +import bloomingblooms.response.CommonResponse; +import kr.bb.store.domain.question.controller.request.AnswerCreateRequest; +import kr.bb.store.domain.question.controller.request.QuestionCreateRequest; +import kr.bb.store.domain.question.controller.response.*; +import kr.bb.store.domain.question.facade.QuestionFacade; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; + + +@RestController +@RequiredArgsConstructor +public class QuestionController { + private final QuestionFacade questionFacade; + + @PostMapping("/questions") + public void createQuestion(@RequestBody QuestionCreateRequest questionCreateRequest, + @RequestHeader(value = "userId") Long userId) { + questionFacade.createQuestion(userId, questionCreateRequest); + } + + @GetMapping("/questions/{questionId}") + public CommonResponse getQuestionDetail(@PathVariable Long questionId) { + return CommonResponse.success(questionFacade.getQuestionInfo(questionId)); + } + + @PostMapping("/questions/{questionId}/answers") + public void createAnswer(@PathVariable Long questionId, @RequestBody AnswerCreateRequest answerCreateRequest) { + questionFacade.createAnswer(questionId, answerCreateRequest.getContent()); + } + + @GetMapping("/{storeId}/questions") + public CommonResponse storeQuestions(@PathVariable Long storeId, + @RequestParam(name = "is-replied", required = false) Boolean isReplied, Pageable pageable) { + return CommonResponse.success(questionFacade.getQuestionsForStoreOwner(storeId,isReplied,pageable)); + } + + @GetMapping("/questions/product/{productId}") + public CommonResponse productQuestions( + @PathVariable String productId,@RequestParam(name = "is-replied", required = false) Boolean isReplied, + Pageable pageable, @RequestHeader(value = "userId", required = false) Long userId) { + return CommonResponse.success(questionFacade.getQuestionsInProduct(userId, productId, isReplied, pageable)); + } + + @GetMapping("/questions/product/{productId}/my") + public CommonResponse myQuestionsInProduct( + @PathVariable String productId, @RequestParam(name = "is-replied", required = false) Boolean isReplied, + Pageable pageable, @RequestHeader(value = "userId") Long userId) { + return CommonResponse.success(questionFacade.getMyQuestionsInProduct(userId, productId, isReplied, pageable)); + } + + @GetMapping("/questions/my-page") + public CommonResponse myQuestions( + @RequestParam(name = "is-replied", required = false) Boolean isReplied, + Pageable pageable, @RequestHeader(value = "userId") Long userId) { + return CommonResponse.success(questionFacade.getMyQuestions(userId, isReplied, pageable)); + } + +} diff --git a/src/main/java/kr/bb/store/domain/question/controller/request/AnswerCreateRequest.java b/src/main/java/kr/bb/store/domain/question/controller/request/AnswerCreateRequest.java new file mode 100644 index 0000000..072efd9 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/controller/request/AnswerCreateRequest.java @@ -0,0 +1,14 @@ +package kr.bb.store.domain.question.controller.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AnswerCreateRequest { + private String content; +} diff --git a/src/main/java/kr/bb/store/domain/question/controller/request/QuestionCreateRequest.java b/src/main/java/kr/bb/store/domain/question/controller/request/QuestionCreateRequest.java new file mode 100644 index 0000000..10319ef --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/controller/request/QuestionCreateRequest.java @@ -0,0 +1,20 @@ +package kr.bb.store.domain.question.controller.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionCreateRequest { + private Long storeId; + private String productName; + private String productId; + private String title; + private String content; + private Boolean isSecret; + private String nickname; +} diff --git a/src/main/java/kr/bb/store/domain/question/controller/response/MyQuestionsInMypagePagingResponse.java b/src/main/java/kr/bb/store/domain/question/controller/response/MyQuestionsInMypagePagingResponse.java new file mode 100644 index 0000000..0303ae2 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/controller/response/MyQuestionsInMypagePagingResponse.java @@ -0,0 +1,28 @@ +package kr.bb.store.domain.question.controller.response; + +import kr.bb.store.domain.question.dto.MyQuestionInMypageDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MyQuestionsInMypagePagingResponse { + private List data; + private Long totalCnt; + + public static MyQuestionsInMypagePagingResponse from(Page myQuestionInMypageDtos) { + return MyQuestionsInMypagePagingResponse.builder() + .data(myQuestionInMypageDtos.getContent()) + .totalCnt(myQuestionInMypageDtos.getTotalElements()) + .build(); + + } + +} diff --git a/src/main/java/kr/bb/store/domain/question/controller/response/MyQuestionsInProductPagingResponse.java b/src/main/java/kr/bb/store/domain/question/controller/response/MyQuestionsInProductPagingResponse.java new file mode 100644 index 0000000..7bcaeb9 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/controller/response/MyQuestionsInProductPagingResponse.java @@ -0,0 +1,26 @@ +package kr.bb.store.domain.question.controller.response; + +import kr.bb.store.domain.question.dto.MyQuestionInMypageDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MyQuestionsInProductPagingResponse { + private List data; + private Long totalCnt; + + public static MyQuestionsInProductPagingResponse from(Page questionInProductDtos) { + return MyQuestionsInProductPagingResponse.builder() + .data(questionInProductDtos.getContent()) + .totalCnt(questionInProductDtos.getTotalElements()) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/question/controller/response/QuestionDetailInfoResponse.java b/src/main/java/kr/bb/store/domain/question/controller/response/QuestionDetailInfoResponse.java new file mode 100644 index 0000000..7a5cd36 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/controller/response/QuestionDetailInfoResponse.java @@ -0,0 +1,38 @@ +package kr.bb.store.domain.question.controller.response; + +import kr.bb.store.domain.question.dto.AnswerDto; +import kr.bb.store.domain.question.entity.Answer; +import kr.bb.store.domain.question.entity.Question; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionDetailInfoResponse { + private String title; + private String nickname; + private LocalDateTime createdAt; + private String productName; + private String content; + private Boolean isReplied; + private AnswerDto answer; + + public static QuestionDetailInfoResponse of(Question question, Answer answer) { + return QuestionDetailInfoResponse.builder() + .title(question.getTitle()) + .nickname(question.getNickname()) + .createdAt(question.getCreatedAt()) + .productName(question.getProductName()) + .content(question.getContent()) + .isReplied(answer != null) + .answer(answer == null ? null : AnswerDto.fromEntity(answer)) + .build(); + } + +} diff --git a/src/main/java/kr/bb/store/domain/question/controller/response/QuestionsForOwnerPagingResponse.java b/src/main/java/kr/bb/store/domain/question/controller/response/QuestionsForOwnerPagingResponse.java new file mode 100644 index 0000000..9072045 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/controller/response/QuestionsForOwnerPagingResponse.java @@ -0,0 +1,26 @@ +package kr.bb.store.domain.question.controller.response; + +import kr.bb.store.domain.question.dto.QuestionForOwnerDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionsForOwnerPagingResponse { + private List data; + private Long totalCnt; + + public static QuestionsForOwnerPagingResponse from(Page questionForOwnerDtos) { + return QuestionsForOwnerPagingResponse.builder() + .data(questionForOwnerDtos.getContent()) + .totalCnt(questionForOwnerDtos.getTotalElements()) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/question/controller/response/QuestionsInProductPagingResponse.java b/src/main/java/kr/bb/store/domain/question/controller/response/QuestionsInProductPagingResponse.java new file mode 100644 index 0000000..939216b --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/controller/response/QuestionsInProductPagingResponse.java @@ -0,0 +1,26 @@ +package kr.bb.store.domain.question.controller.response; + +import kr.bb.store.domain.question.dto.QuestionInProductDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.domain.Page; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionsInProductPagingResponse { + private List data; + private Long totalCnt; + + public static QuestionsInProductPagingResponse from(Page questionInProductDtos) { + return QuestionsInProductPagingResponse.builder() + .data(questionInProductDtos.getContent()) + .totalCnt(questionInProductDtos.getTotalElements()) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/question/dto/AnswerDto.java b/src/main/java/kr/bb/store/domain/question/dto/AnswerDto.java new file mode 100644 index 0000000..c2a431b --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/dto/AnswerDto.java @@ -0,0 +1,25 @@ +package kr.bb.store.domain.question.dto; + +import kr.bb.store.domain.question.entity.Answer; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class AnswerDto { + private String content; + private LocalDate repliedAt; + + public static AnswerDto fromEntity(Answer answer) { + return AnswerDto.builder() + .content(answer.getContent()) + .repliedAt(answer.getUpdatedAt().toLocalDate()) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/question/dto/MyQuestionInMypageDto.java b/src/main/java/kr/bb/store/domain/question/dto/MyQuestionInMypageDto.java new file mode 100644 index 0000000..361a645 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/dto/MyQuestionInMypageDto.java @@ -0,0 +1,38 @@ +package kr.bb.store.domain.question.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MyQuestionInMypageDto { + private Long key; + private Boolean isReplied; + private String title; + private String content; + private String nickname; + private LocalDate createdAt; + private String reply; + private LocalDate repliedAt; + private String productName; + + @QueryProjection + public MyQuestionInMypageDto(Long key, Boolean isReplied, String title, String content, String nickname, LocalDateTime createdAt, String reply, LocalDateTime repliedAt) { + this.key = key; + this.isReplied = isReplied; + this.title = title; + this.content = content; + this.nickname = nickname; + this.createdAt = createdAt.toLocalDate(); + this.reply = reply; + this.repliedAt = repliedAt != null ? repliedAt.toLocalDate() : null; + } +} diff --git a/src/main/java/kr/bb/store/domain/question/dto/QuestionForOwnerDto.java b/src/main/java/kr/bb/store/domain/question/dto/QuestionForOwnerDto.java new file mode 100644 index 0000000..720fdb5 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/dto/QuestionForOwnerDto.java @@ -0,0 +1,35 @@ +package kr.bb.store.domain.question.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionForOwnerDto { + private Long key; + private String productName; + private String nickname; + private String title; + private LocalDate createdAt; + private Boolean isReplied; + private Boolean isRead; + + @QueryProjection + public QuestionForOwnerDto(Long key, String productName, String nickname, String title, LocalDateTime createdAt, Boolean isReplied, Boolean isRead) { + this.key = key; + this.productName = productName; + this.nickname = nickname; + this.title = title; + this.createdAt = createdAt.toLocalDate(); + this.isReplied = isReplied; + this.isRead = isRead; + } +} diff --git a/src/main/java/kr/bb/store/domain/question/dto/QuestionInProductDto.java b/src/main/java/kr/bb/store/domain/question/dto/QuestionInProductDto.java new file mode 100644 index 0000000..c78c0c2 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/dto/QuestionInProductDto.java @@ -0,0 +1,41 @@ +package kr.bb.store.domain.question.dto; + +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class QuestionInProductDto { + private Long key; + private Boolean isReplied; + private String title; + private String content; + private String nickname; + private LocalDate createdAt; + private Boolean isSecret; + private Boolean isMine; + private String reply; + private LocalDate repliedAt; + + @QueryProjection + public QuestionInProductDto(Long key, Boolean isReplied, String title, String content, String nickname, LocalDateTime createdAt, Boolean isSecret, Boolean isMine, String reply, LocalDateTime repliedAt) { + this.key = key; + this.isReplied = isReplied; + this.title = title; + this.content = content; + this.nickname = nickname; + this.createdAt = createdAt.toLocalDate(); + this.isSecret = isSecret; + this.isMine = isMine; + this.reply = reply; + this.repliedAt = repliedAt != null ? repliedAt.toLocalDate() : null; + } +} diff --git a/src/main/java/kr/bb/store/domain/question/entity/Answer.java b/src/main/java/kr/bb/store/domain/question/entity/Answer.java new file mode 100644 index 0000000..983a01f --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/entity/Answer.java @@ -0,0 +1,32 @@ +package kr.bb.store.domain.question.entity; + +import kr.bb.store.domain.common.entity.BaseEntity; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class Answer extends BaseEntity { + @Id + private Long id; + + @MapsId + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name="id") + private Question question; + + @NotNull + private String content; + + @Builder + public Answer(Question question, String content) { + this.question = question; + this.content = content; + } +} diff --git a/src/main/java/kr/bb/store/domain/question/entity/Question.java b/src/main/java/kr/bb/store/domain/question/entity/Question.java new file mode 100644 index 0000000..98b2c3b --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/entity/Question.java @@ -0,0 +1,66 @@ +package kr.bb.store.domain.question.entity; + +import kr.bb.store.domain.common.entity.BaseEntity; +import kr.bb.store.domain.store.entity.Store; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicInsert +@Getter +@Entity +public class Question extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name="store_id") + private Store store; + + @NotNull + private Long userId; + + @NotNull + private String nickname; + + @NotNull + private String productId; + + @NotNull + private String productName; + + @NotNull + private String title; + + @NotNull + private String content; + + @Column(nullable = false, columnDefinition = "boolean default false") + private Boolean isSecret; + + @Column(nullable = false, columnDefinition = "boolean default false") + private Boolean isRead; + + @Builder + public Question(Store store, Long userId, String nickname, String productId, String productName, String title, String content, Boolean isSecret) { + this.store = store; + this.userId = userId; + this.nickname = nickname; + this.productId = productId; + this.productName = productName; + this.title = title; + this.content = content; + this.isSecret = isSecret; + } + + public void check() { + this.isRead = true; + } +} diff --git a/src/main/java/kr/bb/store/domain/question/exception/QuestionNotFoundException.java b/src/main/java/kr/bb/store/domain/question/exception/QuestionNotFoundException.java new file mode 100644 index 0000000..77f777a --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/exception/QuestionNotFoundException.java @@ -0,0 +1,9 @@ +package kr.bb.store.domain.question.exception; + +public class QuestionNotFoundException extends RuntimeException { + public static final String MESSAGE = "ํ•ด๋‹น ๋ฌธ์˜๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; + + public QuestionNotFoundException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/question/facade/QuestionFacade.java b/src/main/java/kr/bb/store/domain/question/facade/QuestionFacade.java new file mode 100644 index 0000000..d7dcc02 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/facade/QuestionFacade.java @@ -0,0 +1,67 @@ +package kr.bb.store.domain.question.facade; + +import kr.bb.store.client.UserFeignClient; +import kr.bb.store.domain.question.controller.request.QuestionCreateRequest; +import kr.bb.store.domain.question.controller.response.*; +import kr.bb.store.domain.question.entity.Question; +import kr.bb.store.domain.question.service.QuestionService; +import kr.bb.store.exception.NonPropagatingException; +import kr.bb.store.message.AnswerSQSPublisher; +import kr.bb.store.message.QuestionSQSPublisher; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class QuestionFacade { + private final QuestionService questionService; + private final QuestionSQSPublisher questionSQSPublisher; + private final AnswerSQSPublisher answerSQSPublisher; + private final UserFeignClient userFeignClient; + + public void createQuestion(Long userId, QuestionCreateRequest questionCreateRequest) { + questionService.createQuestion(userId, questionCreateRequest); + log.info("question from user {} created successfully", userId); + Long storeId = questionCreateRequest.getStoreId(); + questionSQSPublisher.publish(storeId); + } + + public QuestionDetailInfoResponse getQuestionInfo(Long questionId) { + return questionService.getQuestionInfo(questionId); + } + + public void createAnswer(Long questionId, String content) { + Question question = questionService.getQuestionById(questionId); + questionService.createAnswer(question, content); + log.info("answer to question {} created successfully", questionId); + Long userId = question.getUserId(); + try{ + String phoneNumber = userFeignClient.getPhoneNumber(userId).getData(); + answerSQSPublisher.publish(userId, phoneNumber); + }catch (Exception e) { + log.error(e.toString()); + log.warn("{}'s Request of '{}' failed. sqs message may not work properly", "UserFeignClient", "getPhoneNumber"); + throw new NonPropagatingException(e.getMessage()); + } + } + + public QuestionsForOwnerPagingResponse getQuestionsForStoreOwner(Long storeId, Boolean isReplied, Pageable pageable) { + return questionService.getQuestionsForStoreOwner(storeId, isReplied, pageable); + } + + public QuestionsInProductPagingResponse getQuestionsInProduct(Long userId, String productId, Boolean isReplied, Pageable pageable) { + return questionService.getQuestionsInProduct(userId, productId, isReplied, pageable); + } + + public MyQuestionsInProductPagingResponse getMyQuestionsInProduct(Long userId, String productId, Boolean isReplied, Pageable pageable) { + return questionService.getMyQuestionsInProduct(userId, productId, isReplied, pageable); + } + + public MyQuestionsInMypagePagingResponse getMyQuestions(Long userId, Boolean isReplied, Pageable pageable) { + return questionService.getMyQuestions(userId, isReplied, pageable); + } + +} diff --git a/src/main/java/kr/bb/store/domain/question/handler/AnswerCreator.java b/src/main/java/kr/bb/store/domain/question/handler/AnswerCreator.java new file mode 100644 index 0000000..de13c26 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/handler/AnswerCreator.java @@ -0,0 +1,22 @@ +package kr.bb.store.domain.question.handler; + +import kr.bb.store.domain.question.entity.Answer; +import kr.bb.store.domain.question.entity.Question; +import kr.bb.store.domain.question.repository.AnswerRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class AnswerCreator { + private final AnswerRepository answerRepository; + + public Answer create(Question question, String content) { + Answer answer = Answer.builder() + .question(question) + .content(content) + .build(); + + return answerRepository.save(answer); + } +} diff --git a/src/main/java/kr/bb/store/domain/question/handler/QuestionCreator.java b/src/main/java/kr/bb/store/domain/question/handler/QuestionCreator.java new file mode 100644 index 0000000..a8be4e1 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/handler/QuestionCreator.java @@ -0,0 +1,36 @@ +package kr.bb.store.domain.question.handler; + +import kr.bb.store.domain.question.controller.request.QuestionCreateRequest; +import kr.bb.store.domain.question.entity.Question; +import kr.bb.store.domain.question.repository.QuestionRepository; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.exception.StoreNotFoundException; +import kr.bb.store.domain.store.repository.StoreRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class QuestionCreator { + private final QuestionRepository questionRepository; + private final StoreRepository storeRepository; + + public Question create(Long userId, QuestionCreateRequest questionCreateRequest) { + Store store = storeRepository.findById(questionCreateRequest.getStoreId()) + .orElseThrow(StoreNotFoundException::new); + + Question question = Question.builder() + .store(store) + .userId(userId) + .nickname(questionCreateRequest.getNickname()) + .productId(questionCreateRequest.getProductId()) + .productName(questionCreateRequest.getProductName()) + .title(questionCreateRequest.getTitle()) + .content(questionCreateRequest.getContent()) + .isSecret(questionCreateRequest.getIsSecret()) + .build(); + + return questionRepository.save(question); + } + +} diff --git a/src/main/java/kr/bb/store/domain/question/handler/QuestionReader.java b/src/main/java/kr/bb/store/domain/question/handler/QuestionReader.java new file mode 100644 index 0000000..644f609 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/handler/QuestionReader.java @@ -0,0 +1,53 @@ +package kr.bb.store.domain.question.handler; + +import kr.bb.store.domain.question.dto.*; +import kr.bb.store.domain.question.controller.response.QuestionDetailInfoResponse; +import kr.bb.store.domain.question.entity.Answer; +import kr.bb.store.domain.question.entity.Question; +import kr.bb.store.domain.question.exception.QuestionNotFoundException; +import kr.bb.store.domain.question.repository.AnswerRepository; +import kr.bb.store.domain.question.repository.QuestionRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + + +@RequiredArgsConstructor +@Component +public class QuestionReader { + private final QuestionRepository questionRepository; + private final AnswerRepository answerRepository; + + public QuestionDetailInfoResponse readDetailInfo(Long questionId) { + Question question = questionRepository.findById(questionId) + .orElseThrow(QuestionNotFoundException::new); + question.check(); + + Answer answer = answerRepository.findById(questionId) + .orElse(null); + + return QuestionDetailInfoResponse.of(question,answer); + } + + public Page readQuestionsForStoreOwner(Long storeId, Boolean isReplied, Pageable pageable) { + return questionRepository.getQuestionsForStoreOwnerWithPaging(storeId, isReplied, pageable); + } + + public Page readQuestionsInProduct(Long userId, String productId, Boolean isReplied, Pageable pageable) { + return questionRepository.getQuestionsInProductWithPaging(userId, productId, isReplied, pageable); + } + + public Page readMyQuestionsInProduct(Long userId, String productId, Boolean isReplied, Pageable pageable) { + return questionRepository.getMyQuestionsInProductWithPaging(userId, productId, isReplied, pageable); + } + + public Page readQuestionsForMypage(Long userId, Boolean isReplied, Pageable pageable) { + return questionRepository.getMyQuestionsWithPaging(userId, isReplied, pageable); + } + + public Question read(Long questionId) { + return questionRepository.findById(questionId) + .orElseThrow(QuestionNotFoundException::new); + } +} diff --git a/src/main/java/kr/bb/store/domain/question/repository/AnswerRepository.java b/src/main/java/kr/bb/store/domain/question/repository/AnswerRepository.java new file mode 100644 index 0000000..12f4d94 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/repository/AnswerRepository.java @@ -0,0 +1,7 @@ +package kr.bb.store.domain.question.repository; + +import kr.bb.store.domain.question.entity.Answer; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface AnswerRepository extends JpaRepository { +} diff --git a/src/main/java/kr/bb/store/domain/question/repository/QuestionRepository.java b/src/main/java/kr/bb/store/domain/question/repository/QuestionRepository.java new file mode 100644 index 0000000..2d8f997 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/repository/QuestionRepository.java @@ -0,0 +1,7 @@ +package kr.bb.store.domain.question.repository; + +import kr.bb.store.domain.question.entity.Question; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface QuestionRepository extends JpaRepository,QuestionRepositoryCustom { +} diff --git a/src/main/java/kr/bb/store/domain/question/repository/QuestionRepositoryCustom.java b/src/main/java/kr/bb/store/domain/question/repository/QuestionRepositoryCustom.java new file mode 100644 index 0000000..f7f9f9c --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/repository/QuestionRepositoryCustom.java @@ -0,0 +1,14 @@ +package kr.bb.store.domain.question.repository; + +import kr.bb.store.domain.question.dto.MyQuestionInMypageDto; +import kr.bb.store.domain.question.dto.QuestionForOwnerDto; +import kr.bb.store.domain.question.dto.QuestionInProductDto; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface QuestionRepositoryCustom { + Page getQuestionsForStoreOwnerWithPaging(Long storeId, Boolean isReplied, Pageable pageable); + Page getQuestionsInProductWithPaging(Long userId, String productId, Boolean isReplied, Pageable pageable); + Page getMyQuestionsInProductWithPaging(Long userId, String productId, Boolean isReplied, Pageable pageable); + Page getMyQuestionsWithPaging(Long userId, Boolean isReplied, Pageable pageable); +} diff --git a/src/main/java/kr/bb/store/domain/question/repository/QuestionRepositoryCustomImpl.java b/src/main/java/kr/bb/store/domain/question/repository/QuestionRepositoryCustomImpl.java new file mode 100644 index 0000000..0b92398 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/repository/QuestionRepositoryCustomImpl.java @@ -0,0 +1,211 @@ +package kr.bb.store.domain.question.repository; + +import com.querydsl.core.types.Expression; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.CaseBuilder; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.core.types.dsl.StringExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import kr.bb.store.domain.question.dto.*; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import javax.persistence.EntityManager; +import java.util.List; + +import static kr.bb.store.domain.question.entity.QAnswer.answer; +import static kr.bb.store.domain.question.entity.QQuestion.question; + +public class QuestionRepositoryCustomImpl implements QuestionRepositoryCustom{ + private final JPAQueryFactory queryFactory; + + public QuestionRepositoryCustomImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Page getQuestionsForStoreOwnerWithPaging(Long storeId, Boolean isReplied, Pageable pageable) { + List contents = queryFactory.select(new QQuestionForOwnerDto( + question.id, + question.productName, + question.nickname, + question.title, + question.createdAt, + Expressions.asBoolean(answer.question.id.isNotNull()), + question.isRead + )) + .from(answer) + .rightJoin(answer.question, question) + .where( + isReplied != null ? checkRepliedCondition(isReplied) : null, + question.isDeleted.isFalse(), + question.store.id.eq(storeId) + ) + .orderBy(question.createdAt.desc()) + .limit(pageable.getPageSize()) + .offset(pageable.getOffset()) + .fetch(); + + Long count = queryFactory + .select(question.id.count()) + .from(answer) + .rightJoin(answer.question, question) + .where( + isReplied != null ? checkRepliedCondition(isReplied) : null, + question.isDeleted.isFalse(), + question.store.id.eq(storeId) + ) + .fetchOne(); + return new PageImpl<>(contents,pageable,count); + + } + + @Override + public Page getQuestionsInProductWithPaging(Long userId, String productId, Boolean isReplied, Pageable pageable) { + List contents = queryFactory.select(new QQuestionInProductDto( + question.id, + Expressions.asBoolean(answer.question.id.isNotNull()), + makeTitle(userId), + makeContent(userId), + question.nickname, + question.createdAt, + question.isSecret, + isMyQuestion(userId), + answer.content, + answer.createdAt + )) + .from(answer) + .rightJoin(answer.question, question) + .where( + isReplied != null ? checkRepliedCondition(isReplied) : null, + question.productId.eq(productId), + question.isDeleted.isFalse() + ) + .orderBy(question.createdAt.desc()) + .limit(pageable.getPageSize()) + .offset(pageable.getOffset()) + .fetch(); + + Long count = queryFactory + .select(question.id.count()) + .from(answer) + .rightJoin(answer.question, question) + .where( + isReplied != null ? checkRepliedCondition(isReplied) : null, + question.productId.eq(productId), + question.isDeleted.isFalse() + ) + .fetchOne(); + return new PageImpl<>(contents,pageable,count); + + } + + @Override + public Page getMyQuestionsInProductWithPaging(Long userId, String productId, Boolean isReplied, Pageable pageable) { + List contents = queryFactory.select(new QMyQuestionInMypageDto( + question.id, + Expressions.asBoolean(answer.question.id.isNotNull()), + question.title, + question.content, + question.nickname, + question.createdAt, + answer.content, + answer.createdAt + )) + .from(answer) + .rightJoin(answer.question, question) + .where( + isReplied != null ? checkRepliedCondition(isReplied) : null, + question.productId.eq(productId), + question.userId.eq(userId), + question.isDeleted.isFalse() + ) + .orderBy(question.createdAt.desc()) + .limit(pageable.getPageSize()) + .offset(pageable.getOffset()) + .fetch(); + + Long count = queryFactory + .select(question.id.count()) + .from(answer) + .rightJoin(answer.question, question) + .where( + isReplied != null ? checkRepliedCondition(isReplied) : null, + question.productId.eq(productId), + question.userId.eq(userId), + question.isDeleted.isFalse() + ) + .fetchOne(); + return new PageImpl<>(contents,pageable,count); + + } + + @Override + public Page getMyQuestionsWithPaging(Long userId, Boolean isReplied, Pageable pageable) { + List contents = queryFactory.select(new QMyQuestionInMypageDto( + question.id, + Expressions.asBoolean(answer.question.id.isNotNull()), + question.title, + question.content, + question.nickname, + question.createdAt, + answer.content, + answer.createdAt + )) + .from(answer) + .rightJoin(answer.question, question) + .where( + isReplied != null ? checkRepliedCondition(isReplied) : null, + question.userId.eq(userId), + question.isDeleted.isFalse() + ) + .orderBy(question.createdAt.desc()) + .limit(pageable.getPageSize()) + .offset(pageable.getOffset()) + .fetch(); + + Long count = queryFactory + .select(question.id.count()) + .from(answer) + .rightJoin(answer.question, question) + .where( + isReplied != null ? checkRepliedCondition(isReplied) : null, + question.userId.eq(userId), + question.isDeleted.isFalse() + ) + .fetchOne(); + return new PageImpl<>(contents,pageable,count); + + } + + + + private StringExpression makeTitle(Long userId) { + return new CaseBuilder() + .when(question.isSecret.isTrue().and(isNotMyQuestion(userId))) + .then(Expressions.asString("๋น„๋ฐ€๊ธ€์ž…๋‹ˆ๋‹ค")) + .otherwise(question.title); + } + + private StringExpression makeContent(Long userId) { + return new CaseBuilder() + .when(question.isSecret.isTrue().and(isNotMyQuestion(userId))) + .then(Expressions.asString("")) + .otherwise(question.content); + } + + private Expression isMyQuestion(Long userId) { + if(userId == null) return Expressions.asBoolean(false); + return question.userId.eq(userId); + } + + private BooleanExpression isNotMyQuestion(Long userId) { + return (userId == null) ? null : question.userId.ne(userId); + } + + private BooleanExpression checkRepliedCondition(boolean isReplied) { + return isReplied ? answer.question.id.isNotNull() : + answer.question.id.isNull(); + } +} diff --git a/src/main/java/kr/bb/store/domain/question/service/QuestionService.java b/src/main/java/kr/bb/store/domain/question/service/QuestionService.java new file mode 100644 index 0000000..aa6b96e --- /dev/null +++ b/src/main/java/kr/bb/store/domain/question/service/QuestionService.java @@ -0,0 +1,69 @@ +package kr.bb.store.domain.question.service; + +import kr.bb.store.domain.question.controller.request.QuestionCreateRequest; +import kr.bb.store.domain.question.controller.response.*; +import kr.bb.store.domain.question.dto.MyQuestionInMypageDto; +import kr.bb.store.domain.question.dto.QuestionForOwnerDto; +import kr.bb.store.domain.question.dto.QuestionInProductDto; +import kr.bb.store.domain.question.entity.Answer; +import kr.bb.store.domain.question.entity.Question; +import kr.bb.store.domain.question.handler.AnswerCreator; +import kr.bb.store.domain.question.handler.QuestionCreator; +import kr.bb.store.domain.question.handler.QuestionReader; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class QuestionService { + private final QuestionCreator questionCreator; + private final AnswerCreator answerCreator; + private final QuestionReader questionReader; + + @Transactional + public Question createQuestion(Long userId, QuestionCreateRequest questionCreateRequest) { + return questionCreator.create(userId, questionCreateRequest); + } + + @Transactional + public QuestionDetailInfoResponse getQuestionInfo(Long questionId) { + return questionReader.readDetailInfo(questionId); + } + + @Transactional + public Answer createAnswer(Question question, String content) { + return answerCreator.create(question, content); + } + + public QuestionsForOwnerPagingResponse getQuestionsForStoreOwner(Long storeId, Boolean isReplied, Pageable pageable) { + Page questionForOwnerDtos = questionReader.readQuestionsForStoreOwner(storeId, isReplied, pageable); + return QuestionsForOwnerPagingResponse.from(questionForOwnerDtos); + } + + public QuestionsInProductPagingResponse getQuestionsInProduct(Long userId, String productId, Boolean isReplied, Pageable pageable) { + Page questionInProductDtos = questionReader.readQuestionsInProduct(userId, productId, isReplied, pageable); + return QuestionsInProductPagingResponse.from(questionInProductDtos); + } + + public MyQuestionsInProductPagingResponse getMyQuestionsInProduct(Long userId, String productId, Boolean isReplied, Pageable pageable) { + Page questionInProductDtos = questionReader.readMyQuestionsInProduct(userId, productId, isReplied, pageable); + return MyQuestionsInProductPagingResponse.from(questionInProductDtos); + } + + public MyQuestionsInMypagePagingResponse getMyQuestions(Long userId, Boolean isReplied, Pageable pageable) { + Page myQuestionInMypageDtos = questionReader.readQuestionsForMypage(userId, isReplied, pageable); + return MyQuestionsInMypagePagingResponse.builder() + .data(myQuestionInMypageDtos.getContent()) + .totalCnt(myQuestionInMypageDtos.getTotalElements()) + .build(); + } + + public Question getQuestionById(Long questionId) { + return questionReader.read(questionId); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/StoreController.java b/src/main/java/kr/bb/store/domain/store/controller/StoreController.java new file mode 100644 index 0000000..9c95b4b --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/StoreController.java @@ -0,0 +1,93 @@ +package kr.bb.store.domain.store.controller; + + +import bloomingblooms.response.CommonResponse; +import kr.bb.store.domain.store.controller.request.SortType; +import kr.bb.store.domain.store.controller.request.StoreCreateRequest; +import kr.bb.store.domain.store.controller.request.StoreInfoEditRequest; +import kr.bb.store.domain.store.controller.response.*; +import kr.bb.store.domain.store.dto.DeliveryPolicyDto; +import kr.bb.store.domain.store.dto.GugunDto; +import kr.bb.store.domain.store.dto.SidoDto; +import kr.bb.store.domain.store.facade.StoreFacade; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.web.bind.annotation.*; + +import javax.validation.Valid; +import java.util.List; + +@RestController +@RequiredArgsConstructor +public class StoreController { + private final StoreFacade storeFacade; + + @PostMapping + public CommonResponse createStore(@Valid @RequestBody StoreCreateRequest storeCreateRequest, + @RequestHeader(value = "userId") Long userId) { + return CommonResponse.success(storeFacade.createStore(userId, storeCreateRequest)); + } + + @PutMapping("/{storeId}") + public void editStoreInfo(@PathVariable Long storeId, + @Valid @RequestBody StoreInfoEditRequest storeInfoEditRequest) { + storeFacade.editStoreInfo(storeId, storeInfoEditRequest); + } + + @GetMapping("/{storeId}") + public CommonResponse getStoreInfo(@PathVariable Long storeId) { + return CommonResponse.success(storeFacade.getStoreDetailInfo(storeId)); + } + + @GetMapping("/list") + public CommonResponse getStores( + @RequestHeader(value = "userId", required = false) Long userId, Pageable pageable) { + return CommonResponse.success(storeFacade.getStoresWithLikes(userId, pageable)); + } + + @GetMapping("/{storeId}/user") + public CommonResponse getStoreInfoForUser( + @RequestHeader(value = "userId", required = false) Long userId, @PathVariable Long storeId){ + return CommonResponse.success(storeFacade.getStoreInfoForUser(userId, storeId)); + } + + @GetMapping("/{storeId}/manager") + public CommonResponse getStoreInfoForManager(@PathVariable Long storeId){ + return CommonResponse.success(storeFacade.getStoreInfoForManager(storeId)); + } + + @GetMapping("/map/location") + public CommonResponse getNearbyStores( + @RequestParam Double lat, @RequestParam Double lon, + @RequestHeader(value = "userId", required = false) Long userId,@RequestParam Integer level) { + return CommonResponse.success(storeFacade.getNearbyStores(userId, lat, lon, level)); + } + + @GetMapping("/map/region") + public CommonResponse getStoresWithRegion( + @RequestParam String sido, @RequestParam String gugun, + @RequestHeader(value = "userId", required = false) Long userId) { + return CommonResponse.success(storeFacade.getStoresWithRegion(userId, sido, gugun)); + } + + @GetMapping("/{storeId}/delivery-policy") + public CommonResponse getDeliveryPolicy(@PathVariable Long storeId) { + return CommonResponse.success(storeFacade.getDeliveryPolicy(storeId)); + } + + @GetMapping("/admin") + public CommonResponse getStoresForAdmin(Pageable pageable, @RequestParam(required = false) SortType sort, + @RequestParam String sido, @RequestParam(required = false, defaultValue = "") String gugun) { + return CommonResponse.success(storeFacade.getStoresForAdmin(pageable, sort, sido, gugun)); + } + + @GetMapping("/address/sido") + public CommonResponse> getAllSido() { + return CommonResponse.success(storeFacade.getAllSido()); + } + + @GetMapping("/address/gugun") + public CommonResponse> getGugun(@RequestParam String sido) { + return CommonResponse.success(storeFacade.getGuguns(sido)); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/StoreFeignController.java b/src/main/java/kr/bb/store/domain/store/controller/StoreFeignController.java new file mode 100644 index 0000000..2c3683b --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/StoreFeignController.java @@ -0,0 +1,75 @@ +package kr.bb.store.domain.store.controller; + +import bloomingblooms.domain.order.ValidatePolicyDto; +import bloomingblooms.domain.store.StoreInfoDto; +import bloomingblooms.domain.store.StoreNameAndAddressDto; +import bloomingblooms.domain.store.StorePolicy; +import bloomingblooms.domain.wishlist.likes.LikedStoreInfoResponse; +import bloomingblooms.dto.response.SettlementStoreInfoResponse; +import bloomingblooms.response.CommonResponse; +import kr.bb.store.domain.store.facade.StoreFacade; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.Map; + +@CrossOrigin(origins = "*") +@RestController +@RequiredArgsConstructor +@RequestMapping("/client/stores") +public class StoreFeignController { + private final StoreFacade storeFacade; + + @GetMapping("/id") + public CommonResponse getStoreId(@RequestHeader Long userId) { + return CommonResponse.success(storeFacade.getStoreId(userId)); + } + + @GetMapping("/{storeId}/info") + public CommonResponse getStoreNameAndAddress(@PathVariable Long storeId) { + return CommonResponse.success(storeFacade.getStoreNameAndAddress(storeId)); + } + + @GetMapping("/{storeId}/name") + public CommonResponse getStoreName(@PathVariable Long storeId) { + return CommonResponse.success(storeFacade.getStoreName(storeId)); + } + + @GetMapping("/store-name") + public CommonResponse> getStoreNames(@RequestParam List storeIds) { + return CommonResponse.success(storeFacade.getStoreNames(storeIds)); + } + + @GetMapping("/{storeId}") + public CommonResponse getStoreInfo(@PathVariable Long storeId) { + return CommonResponse.success(storeFacade.getStoreInfo(storeId)); + } + + @GetMapping + public CommonResponse> getStoreInfos() { + return CommonResponse.success(storeFacade.getAllStoreInfos()); + } + + @PostMapping("/coupons/validate-purchase") + public CommonResponse validateForOrder(@RequestBody ValidatePolicyDto validatePolicyDto) { + storeFacade.validateForOrder(validatePolicyDto); + return CommonResponse.success(null); + } + + @PostMapping("/simple-info") + public CommonResponse> getStoreSimpleInfos(@RequestBody List storeIds) { + return CommonResponse.success(storeFacade.simpleInfos(storeIds)); + } + + @PostMapping("/settlements") + public CommonResponse> getStoreInfoForSettlement(@RequestBody List storeIds) { + return CommonResponse.success(storeFacade.storeInfoForSettlement(storeIds)); + } + + @PostMapping("/policy") + public CommonResponse> getDeliveryPolicyOfStores(@RequestBody List storeIds) { + return CommonResponse.success(storeFacade.getDeliveryPolicies(storeIds)); + } + +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/request/SortType.java b/src/main/java/kr/bb/store/domain/store/controller/request/SortType.java new file mode 100644 index 0000000..8594c06 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/request/SortType.java @@ -0,0 +1,7 @@ +package kr.bb.store.domain.store.controller.request; + +public enum SortType { + DATE, + RATE, + AMOUNT; +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/request/StoreCreateRequest.java b/src/main/java/kr/bb/store/domain/store/controller/request/StoreCreateRequest.java new file mode 100644 index 0000000..816495d --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/request/StoreCreateRequest.java @@ -0,0 +1,80 @@ +package kr.bb.store.domain.store.controller.request; + +import kr.bb.store.domain.store.dto.DeliveryPolicyRequestDto; +import kr.bb.store.domain.store.dto.StoreAddressDto; +import kr.bb.store.domain.store.dto.StoreDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.NotNull; +import javax.validation.constraints.PositiveOrZero; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreCreateRequest { + @NotNull(message = "storeName cannot be null") + private String storeName; + @NotNull(message = "detailInfo cannot be null") + private String detailInfo; + @NotNull(message = "storeThumbnailImage cannot be null") + private String storeThumbnailImage; + @NotNull(message = "phoneNumber cannot be null") + private String phoneNumber; + @NotNull(message = "accountNumber cannot be null") + private String accountNumber; + @NotNull(message = "bank cannot be null") + private String bank; + + @PositiveOrZero(message = "deliveryPrice cannot be negative") + private Long deliveryPrice; + @PositiveOrZero(message = "freeDeliveryMinPrice cannot be negative") + private Long freeDeliveryMinPrice; + + @NotNull(message = "sido cannot be null") + private String sido; + @NotNull(message = "gugun cannot be null") + private String gugun; + @NotNull(message = "address cannot be null") + private String address; + @NotNull(message = "detailAddress cannot be null") + private String detailAddress; + @NotNull(message = "zipCode cannot be null") + private String zipCode; + @NotNull(message = "lat cannot be null") + private Double lat; + @NotNull(message = "lon cannot be null") + private Double lon; + + public StoreDto toStoreRequest() { + return StoreDto.builder() + .storeName(storeName) + .detailInfo(detailInfo) + .storeThumbnailImage(storeThumbnailImage) + .phoneNumber(phoneNumber) + .accountNumber(accountNumber) + .bank(bank) + .build(); + } + public DeliveryPolicyRequestDto toDeliveryPolicyRequest() { + return DeliveryPolicyRequestDto.builder() + .deliveryPrice(deliveryPrice) + .freeDeliveryMinPrice(freeDeliveryMinPrice) + .build(); + + } + public StoreAddressDto toStoreAddressRequest() { + return StoreAddressDto.builder() + .sido(sido) + .gugun(gugun) + .address(address) + .detailAddress(detailAddress) + .zipCode(zipCode) + .lat(lat) + .lon(lon) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/request/StoreInfoEditRequest.java b/src/main/java/kr/bb/store/domain/store/controller/request/StoreInfoEditRequest.java new file mode 100644 index 0000000..8b1f8e7 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/request/StoreInfoEditRequest.java @@ -0,0 +1,34 @@ +package kr.bb.store.domain.store.controller.request; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.validation.constraints.PositiveOrZero; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreInfoEditRequest { + private String storeName; + private String detailInfo; + private String storeThumbnailImage; + private String phoneNumber; + private String accountNumber; + private String bank; + + @PositiveOrZero(message = "deliveryPrice cannot be negative") + private Long deliveryPrice; + @PositiveOrZero(message = "freeDeliveryMinPrice cannot be negative") + private Long freeDeliveryMinPrice; + + private String sido; + private String gugun; + private String address; + private String detailAddress; + private String zipCode; + private Double lat; + private Double lon; +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/response/LikedStoreInfoResponse.java b/src/main/java/kr/bb/store/domain/store/controller/response/LikedStoreInfoResponse.java new file mode 100644 index 0000000..fb5a521 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/response/LikedStoreInfoResponse.java @@ -0,0 +1,39 @@ +package kr.bb.store.domain.store.controller.response; + +import kr.bb.store.domain.store.entity.Store; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LikedStoreInfoResponse { + private Long storeId; + private String storeName; + private String detailInfo; + private String storeThumbnail; + private Float averageRating; + + public static LikedStoreInfoResponse fromEntity(Store store) { + return LikedStoreInfoResponse.builder() + .storeId(store.getId()) + .storeName(store.getStoreName()) + .detailInfo(store.getDetailInfo()) + .storeThumbnail(store.getStoreThumbnailImage()) + .averageRating(store.getAverageRating().floatValue()) + .build(); + } + + public bloomingblooms.domain.wishlist.likes.LikedStoreInfoResponse toCommonDto() { + return bloomingblooms.domain.wishlist.likes.LikedStoreInfoResponse.builder() + .storeId(storeId) + .storeName(storeName) + .detailInfo(detailInfo) + .storeThumbnail(storeThumbnail) + .averageRating(averageRating) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/response/SimpleStorePagingResponse.java b/src/main/java/kr/bb/store/domain/store/controller/response/SimpleStorePagingResponse.java new file mode 100644 index 0000000..038aaa1 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/response/SimpleStorePagingResponse.java @@ -0,0 +1,17 @@ +package kr.bb.store.domain.store.controller.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SimpleStorePagingResponse { + private List stores; + private Long totalCnt; +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/response/StoreDetailInfoResponse.java b/src/main/java/kr/bb/store/domain/store/controller/response/StoreDetailInfoResponse.java new file mode 100644 index 0000000..0b5df51 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/response/StoreDetailInfoResponse.java @@ -0,0 +1,53 @@ +package kr.bb.store.domain.store.controller.response; + +import kr.bb.store.domain.store.entity.DeliveryPolicy; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreDetailInfoResponse { + private String storeName; + private String detailInfo; + private String storeThumbnailImage; + private String phoneNumber; + private String accountNumber; + private String bank; + + private Long deliveryPrice; + private Long freeDeliveryMinPrice; + + private String sido; + private String gugun; + private String address; + private String detailAddress; + private String zipCode; + private Double lat; + private Double lon; + + public static StoreDetailInfoResponse of(Store store, DeliveryPolicy deliveryPolicy, StoreAddress storeAddress) { + return StoreDetailInfoResponse.builder() + .storeName(store.getStoreName()) + .detailInfo(store.getDetailInfo()) + .storeThumbnailImage(store.getStoreThumbnailImage()) + .phoneNumber(store.getPhoneNumber()) + .accountNumber(store.getAccountNumber()) + .bank(store.getBank()) + .deliveryPrice(deliveryPolicy.getDeliveryPrice()) + .freeDeliveryMinPrice(deliveryPolicy.getFreeDeliveryMinPrice()) + .sido(storeAddress.getSido().getName()) + .gugun(storeAddress.getGugun().getName()) + .address(storeAddress.getAddress()) + .detailAddress(storeAddress.getDetailAddress()) + .zipCode(storeAddress.getZipCode()) + .lat(storeAddress.getLat()) + .lon(storeAddress.getLon()) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/response/StoreForAdminDtoResponse.java b/src/main/java/kr/bb/store/domain/store/controller/response/StoreForAdminDtoResponse.java new file mode 100644 index 0000000..393a303 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/response/StoreForAdminDtoResponse.java @@ -0,0 +1,25 @@ +package kr.bb.store.domain.store.controller.response; + +import kr.bb.store.domain.store.dto.StoreForAdminDto; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreForAdminDtoResponse { + private List data; + private Long totalCnt; + + public static StoreForAdminDtoResponse of(List data, Long totalCnt) { + return StoreForAdminDtoResponse.builder() + .data(data) + .totalCnt(totalCnt) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/response/StoreForMapResponse.java b/src/main/java/kr/bb/store/domain/store/controller/response/StoreForMapResponse.java new file mode 100644 index 0000000..8199fed --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/response/StoreForMapResponse.java @@ -0,0 +1,42 @@ +package kr.bb.store.domain.store.controller.response; + +import com.querydsl.core.annotations.QueryProjection; +import kr.bb.store.domain.store.dto.Position; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreForMapResponse { + private Long storeId; + private String storeName; + private Boolean isLiked; + private String detailInfo; + private String thumbnailImage; + private Double averageRating; + private Position position; + private String address; + private String detailAddress; + + @QueryProjection + public StoreForMapResponse(Long storeId, String storeName,String detailInfo, String thumbnailImage, + Double averageRating, Double lat, Double lon, + String address, String detailAddress) { + this.storeId = storeId; + this.storeName = storeName; + this.detailInfo = detailInfo; + this.thumbnailImage = thumbnailImage; + this.averageRating = averageRating; + this.position = new Position(lat,lon); + this.address = address; + this.detailAddress = detailAddress; + } + + public void setIsLiked(Boolean isLiked) { + this.isLiked = isLiked; + } +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/response/StoreInfoManagerResponse.java b/src/main/java/kr/bb/store/domain/store/controller/response/StoreInfoManagerResponse.java new file mode 100644 index 0000000..41e8c0a --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/response/StoreInfoManagerResponse.java @@ -0,0 +1,36 @@ +package kr.bb.store.domain.store.controller.response; + +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreInfoManagerResponse { + private String storeName; + private String storeThumbnailImage; + private String phoneNumber; + private String accountNumber; + private String bank; + private String detailInfo; + private String address; + private String addressDetail; + + public static StoreInfoManagerResponse of(Store store, StoreAddress storeAddress) { + return StoreInfoManagerResponse.builder() + .storeName(store.getStoreName()) + .storeThumbnailImage(store.getStoreThumbnailImage()) + .phoneNumber(store.getPhoneNumber()) + .accountNumber(store.getAccountNumber()) + .bank(store.getBank()) + .detailInfo(store.getDetailInfo()) + .address(storeAddress.getAddress()) + .addressDetail(storeAddress.getDetailAddress()) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/response/StoreInfoUserResponse.java b/src/main/java/kr/bb/store/domain/store/controller/response/StoreInfoUserResponse.java new file mode 100644 index 0000000..32ef027 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/response/StoreInfoUserResponse.java @@ -0,0 +1,41 @@ +package kr.bb.store.domain.store.controller.response; + +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreInfoUserResponse { + private String storeName; + private String storeThumbnailImage; + private String address; + private String detailAddress; + private Double averageRating; + private String detailInfo; + private String phoneNumber; + private Boolean isLiked; + private Boolean isSubscribed; + private String subscriptionProductId; + + public static StoreInfoUserResponse of(Store store, StoreAddress storeAddress, Boolean isLiked, + Boolean isSubscribed, String subscriptionProductId) { + return StoreInfoUserResponse.builder() + .storeName(store.getStoreName()) + .storeThumbnailImage(store.getStoreThumbnailImage()) + .address(storeAddress.getAddress()) + .detailAddress(storeAddress.getDetailAddress()) + .averageRating(store.getAverageRating()) + .detailInfo(store.getDetailInfo()) + .phoneNumber(store.getPhoneNumber()) + .isLiked(isLiked) + .isSubscribed(isSubscribed) + .subscriptionProductId(subscriptionProductId) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/response/StoreListForMapResponse.java b/src/main/java/kr/bb/store/domain/store/controller/response/StoreListForMapResponse.java new file mode 100644 index 0000000..ea5cbba --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/response/StoreListForMapResponse.java @@ -0,0 +1,29 @@ +package kr.bb.store.domain.store.controller.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreListForMapResponse { + List stores; + + public void setLikes(Map storeLikes) { + stores.forEach(store -> store.setIsLiked(storeLikes.get(store.getStoreId()))); + } + + public List getStoreIds() { + return stores.stream() + .map(StoreForMapResponse::getStoreId) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/kr/bb/store/domain/store/controller/response/StoreListResponse.java b/src/main/java/kr/bb/store/domain/store/controller/response/StoreListResponse.java new file mode 100644 index 0000000..044c7bd --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/controller/response/StoreListResponse.java @@ -0,0 +1,38 @@ +package kr.bb.store.domain.store.controller.response; + + +import com.querydsl.core.annotations.QueryProjection; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +public class StoreListResponse { + private Long storeId; + private String storeThumbnailImage; + private String storeName; + private String detailInfo; + private Double averageRating; + private Boolean isLiked; + private String address; + private String detailAddress; + + @QueryProjection + public StoreListResponse(Long storeId, String storeThumbnailImage, String storeName, String detailInfo, Double averageRating, Boolean isLiked, String address, String detailAddress) { + this.storeId = storeId; + this.storeThumbnailImage = storeThumbnailImage; + this.storeName = storeName; + this.detailInfo = detailInfo; + this.averageRating = averageRating; + this.isLiked = isLiked; + this.address = address; + this.detailAddress = detailAddress; + } + + public void setIsLiked(Boolean isLiked) { + this.isLiked = isLiked; + } +} diff --git a/src/main/java/kr/bb/store/domain/store/dto/DeliveryPolicyDto.java b/src/main/java/kr/bb/store/domain/store/dto/DeliveryPolicyDto.java new file mode 100644 index 0000000..6a1630b --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/dto/DeliveryPolicyDto.java @@ -0,0 +1,23 @@ +package kr.bb.store.domain.store.dto; + +import kr.bb.store.domain.store.entity.DeliveryPolicy; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DeliveryPolicyDto { + private Long deliveryPrice; + private Long freeDeliveryMinPrice; + + public static DeliveryPolicyDto fromEntity(DeliveryPolicy deliveryPolicy) { + return DeliveryPolicyDto.builder() + .deliveryPrice(deliveryPolicy.getDeliveryPrice()) + .freeDeliveryMinPrice(deliveryPolicy.getFreeDeliveryMinPrice()) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/dto/DeliveryPolicyRequestDto.java b/src/main/java/kr/bb/store/domain/store/dto/DeliveryPolicyRequestDto.java new file mode 100644 index 0000000..c3feb93 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/dto/DeliveryPolicyRequestDto.java @@ -0,0 +1,15 @@ +package kr.bb.store.domain.store.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class DeliveryPolicyRequestDto { + private Long deliveryPrice; + private Long freeDeliveryMinPrice; +} \ No newline at end of file diff --git a/src/main/java/kr/bb/store/domain/store/dto/GugunDto.java b/src/main/java/kr/bb/store/domain/store/dto/GugunDto.java new file mode 100644 index 0000000..0494bb4 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/dto/GugunDto.java @@ -0,0 +1,23 @@ +package kr.bb.store.domain.store.dto; + +import kr.bb.store.domain.store.entity.address.Gugun; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GugunDto { + private String value; + private String label; + public static GugunDto fromEntity(Gugun gugun) { + return GugunDto.builder() + .value(gugun.getCode()) + .label(gugun.getName()) + .build(); + } + +} diff --git a/src/main/java/kr/bb/store/domain/store/dto/Position.java b/src/main/java/kr/bb/store/domain/store/dto/Position.java new file mode 100644 index 0000000..9ea14b8 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/dto/Position.java @@ -0,0 +1,30 @@ +package kr.bb.store.domain.store.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.Objects; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Position { + private Double lat; + private Double lon; + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Position position = (Position) o; + return Objects.equals(lat, position.lat) && Objects.equals(lon, position.lon); + } + + @Override + public int hashCode() { + return Objects.hash(lat, lon); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/dto/SidoDto.java b/src/main/java/kr/bb/store/domain/store/dto/SidoDto.java new file mode 100644 index 0000000..d009c18 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/dto/SidoDto.java @@ -0,0 +1,24 @@ +package kr.bb.store.domain.store.dto; + +import kr.bb.store.domain.store.entity.address.Sido; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SidoDto { + private String code; + private String name; + + public static SidoDto fromEntity(Sido sido) { + return SidoDto.builder() + .code(sido.getCode()) + .name(sido.getName()) + .build(); + } + +} diff --git a/src/main/java/kr/bb/store/domain/store/dto/StoreAddressDto.java b/src/main/java/kr/bb/store/domain/store/dto/StoreAddressDto.java new file mode 100644 index 0000000..5d2652c --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/dto/StoreAddressDto.java @@ -0,0 +1,20 @@ +package kr.bb.store.domain.store.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreAddressDto { + private String sido; + private String gugun; + private String address; + private String detailAddress; + private String zipCode; + private Double lat; + private Double lon; +} diff --git a/src/main/java/kr/bb/store/domain/store/dto/StoreDto.java b/src/main/java/kr/bb/store/domain/store/dto/StoreDto.java new file mode 100644 index 0000000..376c240 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/dto/StoreDto.java @@ -0,0 +1,20 @@ +package kr.bb.store.domain.store.dto; + + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreDto { + private String storeName; + private String detailInfo; + private String storeThumbnailImage; + private String phoneNumber; + private String accountNumber; + private String bank; +} diff --git a/src/main/java/kr/bb/store/domain/store/dto/StoreForAdminDto.java b/src/main/java/kr/bb/store/domain/store/dto/StoreForAdminDto.java new file mode 100644 index 0000000..1750e58 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/dto/StoreForAdminDto.java @@ -0,0 +1,39 @@ +package kr.bb.store.domain.store.dto; + +import kr.bb.store.domain.store.entity.Store; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class StoreForAdminDto { + private Long key; + private String storeCode; + private String storeName; + private String phoneNumber; + private String bank; + private String accountNumber; + private Double averageRating; + private Long totalAmount; + private LocalDate regDate; + + public static StoreForAdminDto fromEntity(Store store) { + return StoreForAdminDto.builder() + .key(store.getId()) + .storeCode(store.getStoreCode()) + .storeName(store.getStoreName()) + .phoneNumber(store.getPhoneNumber()) + .bank(store.getBank()) + .accountNumber(store.getAccountNumber()) + .averageRating(store.getAverageRating()) + .totalAmount(store.getMonthlySalesRevenue()) + .regDate(store.getCreatedAt().toLocalDate()) + .build(); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/entity/DeliveryPolicy.java b/src/main/java/kr/bb/store/domain/store/entity/DeliveryPolicy.java new file mode 100644 index 0000000..628f826 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/entity/DeliveryPolicy.java @@ -0,0 +1,47 @@ +package kr.bb.store.domain.store.entity; + +import kr.bb.store.domain.common.entity.BaseEntity; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class DeliveryPolicy extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name="store_id") + private Store store; + + @NotNull + private Long freeDeliveryMinPrice; + + @NotNull + private Long deliveryPrice; + + @Builder + public DeliveryPolicy(Store store, Long freeDeliveryMinPrice, Long deliveryPrice) { + this.store = store; + this.freeDeliveryMinPrice = freeDeliveryMinPrice; + this.deliveryPrice = deliveryPrice; + } + + public void update(Long deliveryPrice, Long freeDeliveryMinPrice) { + this.deliveryPrice = deliveryPrice; + this.freeDeliveryMinPrice = freeDeliveryMinPrice; + } + + public boolean isRightDeliveryPrice(long receivedPaymentPrice, long receivedDeliveryPrice) { + return (freeDeliveryMinPrice <= receivedPaymentPrice && receivedDeliveryPrice == 0) || + (freeDeliveryMinPrice > receivedPaymentPrice && receivedDeliveryPrice == deliveryPrice); + + } +} diff --git a/src/main/java/kr/bb/store/domain/store/entity/Store.java b/src/main/java/kr/bb/store/domain/store/entity/Store.java new file mode 100644 index 0000000..a549463 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/entity/Store.java @@ -0,0 +1,83 @@ +package kr.bb.store.domain.store.entity; + +import kr.bb.store.domain.common.entity.BaseEntity; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; + +import javax.annotation.Nullable; +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicInsert +@Getter +@Entity +public class Store extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @NotNull + private Long storeManagerId; + + @NotNull + private String storeCode; + + @NotNull + private String storeName; + + @NotNull + private String detailInfo; + + @NotNull + private String storeThumbnailImage; + + @Column(nullable = false, columnDefinition = "float default 0.0") + private Double averageRating; + + @NotNull + private String phoneNumber; + + @NotNull + private String accountNumber; + + @NotNull + private String bank; + + @Column(nullable = false, columnDefinition = "bigint default 0") + private Long monthlySalesRevenue; + + @Builder + public Store(Long storeManagerId, String storeCode, String storeName, String detailInfo, + String storeThumbnailImage, String phoneNumber, String accountNumber, String bank) { + this.storeManagerId = storeManagerId; + this.storeCode = storeCode; + this.storeName = storeName; + this.detailInfo = detailInfo; + this.storeThumbnailImage = storeThumbnailImage; + this.phoneNumber = phoneNumber; + this.accountNumber = accountNumber; + this.bank = bank; + } + + public void update(String storeName, String detailInfo, String storeThumbnailImage, + String phoneNumber, String accountNumber, String bank) { + this.storeName = storeName; + this.detailInfo = detailInfo; + this.storeThumbnailImage = storeThumbnailImage; + this.phoneNumber = phoneNumber; + this.accountNumber = accountNumber; + this.bank = bank; + } + + public void updateAverageRating(Double averageRating) { + this.averageRating = averageRating; + } + + public void updateMonthlySalesRevenue(Long monthlySalesRevenue) { + this.monthlySalesRevenue = monthlySalesRevenue; + } +} diff --git a/src/main/java/kr/bb/store/domain/store/entity/StoreAddress.java b/src/main/java/kr/bb/store/domain/store/entity/StoreAddress.java new file mode 100644 index 0000000..09fab08 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/entity/StoreAddress.java @@ -0,0 +1,73 @@ +package kr.bb.store.domain.store.entity; + +import kr.bb.store.domain.common.entity.BaseEntity; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.Sido; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; +import javax.validation.constraints.NotNull; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class StoreAddress extends BaseEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name="store_id") + private Store store; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name="sido_code") + private Sido sido; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name="gugun_code") + private Gugun gugun; + + @NotNull + private String address; + + @NotNull + private String detailAddress; + + @NotNull + private String zipCode; + + @NotNull + private Double lat; + + @NotNull + private Double lon; + + @Builder + public StoreAddress(Store store, Sido sido, Gugun gugun, String address, + String detailAddress, String zipCode, Double lat, Double lon) { + this.store = store; + this.sido = sido; + this.gugun = gugun; + this.address = address; + this.detailAddress = detailAddress; + this.zipCode = zipCode; + this.lat = lat; + this.lon = lon; + } + + public void update(Sido sido, Gugun gugun, String address, String detailAddress, + String zipCode, Double lat, Double lon) { + this.sido = sido; + this.gugun = gugun; + this.address = address; + this.detailAddress = detailAddress; + this.zipCode = zipCode; + this.lat = lat; + this.lon = lon; + } + +} diff --git a/src/main/java/kr/bb/store/domain/store/entity/address/Gugun.java b/src/main/java/kr/bb/store/domain/store/entity/address/Gugun.java new file mode 100644 index 0000000..f1aa6cc --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/entity/address/Gugun.java @@ -0,0 +1,23 @@ +package kr.bb.store.domain.store.entity.address; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.*; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class Gugun { + @Id + private String code; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sido_code", nullable = false) + private Sido sido; + + private String name; +} diff --git a/src/main/java/kr/bb/store/domain/store/entity/address/GugunRepository.java b/src/main/java/kr/bb/store/domain/store/entity/address/GugunRepository.java new file mode 100644 index 0000000..584c1a0 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/entity/address/GugunRepository.java @@ -0,0 +1,12 @@ +package kr.bb.store.domain.store.entity.address; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface GugunRepository extends JpaRepository { + + List findGugunBySidoCode(String sidoCode); + Optional findBySidoAndName(Sido sido, String name); +} diff --git a/src/main/java/kr/bb/store/domain/store/entity/address/Sido.java b/src/main/java/kr/bb/store/domain/store/entity/address/Sido.java new file mode 100644 index 0000000..1830d50 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/entity/address/Sido.java @@ -0,0 +1,20 @@ +package kr.bb.store.domain.store.entity.address; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import javax.persistence.Entity; +import javax.persistence.Id; + +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@Entity +public class Sido { + @Id + private String code; + + private String name; +} diff --git a/src/main/java/kr/bb/store/domain/store/entity/address/SidoRepository.java b/src/main/java/kr/bb/store/domain/store/entity/address/SidoRepository.java new file mode 100644 index 0000000..12d5414 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/entity/address/SidoRepository.java @@ -0,0 +1,9 @@ +package kr.bb.store.domain.store.entity.address; + +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface SidoRepository extends JpaRepository { + Optional findByName(String name); +} diff --git a/src/main/java/kr/bb/store/domain/store/exception/CannotOwnMultipleStoreException.java b/src/main/java/kr/bb/store/domain/store/exception/CannotOwnMultipleStoreException.java new file mode 100644 index 0000000..ed13793 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/exception/CannotOwnMultipleStoreException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.store.exception; + +import kr.bb.store.exception.CustomException; + +public class CannotOwnMultipleStoreException extends CustomException { + public static final String MESSAGE = "๋‘˜ ์ด์ƒ์˜ ๊ฐ€๊ฒŒ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."; + + public CannotOwnMultipleStoreException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/exception/DeliveryInconsistencyException.java b/src/main/java/kr/bb/store/domain/store/exception/DeliveryInconsistencyException.java new file mode 100644 index 0000000..a87a371 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/exception/DeliveryInconsistencyException.java @@ -0,0 +1,12 @@ +package kr.bb.store.domain.store.exception; + +import kr.bb.store.exception.CustomException; + +public class DeliveryInconsistencyException extends CustomException { + + private static final String MESSAGE = "์ฃผ๋ฌธ ์š”์ฒญ์ด ๋ฐฐ์†ก ์ •์ฑ…์„ ์œ„๋ฐ˜ํ–ˆ์Šต๋‹ˆ๋‹ค."; + + public DeliveryInconsistencyException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/exception/DeliveryPolicyNotFoundException.java b/src/main/java/kr/bb/store/domain/store/exception/DeliveryPolicyNotFoundException.java new file mode 100644 index 0000000..ca65561 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/exception/DeliveryPolicyNotFoundException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.store.exception; + +import kr.bb.store.exception.CustomException; + +public class DeliveryPolicyNotFoundException extends CustomException { + public static final String MESSAGE = "ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ๋ฐฐ์†ก์ •์ฑ…์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + + public DeliveryPolicyNotFoundException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/exception/StoreAddressNotFoundException.java b/src/main/java/kr/bb/store/domain/store/exception/StoreAddressNotFoundException.java new file mode 100644 index 0000000..d7c915e --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/exception/StoreAddressNotFoundException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.store.exception; + +import kr.bb.store.exception.CustomException; + +public class StoreAddressNotFoundException extends CustomException { + public static final String MESSAGE = "ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ์ฃผ์†Œ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + + public StoreAddressNotFoundException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/exception/StoreNotFoundException.java b/src/main/java/kr/bb/store/domain/store/exception/StoreNotFoundException.java new file mode 100644 index 0000000..985e9ca --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/exception/StoreNotFoundException.java @@ -0,0 +1,11 @@ +package kr.bb.store.domain.store.exception; + +import kr.bb.store.exception.CustomException; + +public class StoreNotFoundException extends CustomException { + public static final String MESSAGE = "ํ•ด๋‹น ๊ฐ€๊ฒŒ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + + public StoreNotFoundException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/exception/address/GugunNotFoundException.java b/src/main/java/kr/bb/store/domain/store/exception/address/GugunNotFoundException.java new file mode 100644 index 0000000..9920ae5 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/exception/address/GugunNotFoundException.java @@ -0,0 +1,10 @@ +package kr.bb.store.domain.store.exception.address; + +import kr.bb.store.exception.CustomException; + +public class GugunNotFoundException extends CustomException { + public static final String MESSAGE = "ํ•ด๋‹น ๊ตฌ/๊ตฐ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + public GugunNotFoundException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/exception/address/InvalidParentException.java b/src/main/java/kr/bb/store/domain/store/exception/address/InvalidParentException.java new file mode 100644 index 0000000..806f08a --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/exception/address/InvalidParentException.java @@ -0,0 +1,10 @@ +package kr.bb.store.domain.store.exception.address; + +import kr.bb.store.exception.CustomException; + +public class InvalidParentException extends CustomException { + public static final String MESSAGE = "์„ ํƒํ•œ ์‹œ/๋„์™€ ๊ตฌ/๊ตฐ์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + public InvalidParentException() { + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/exception/address/SidoNotFoundException.java b/src/main/java/kr/bb/store/domain/store/exception/address/SidoNotFoundException.java new file mode 100644 index 0000000..885e772 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/exception/address/SidoNotFoundException.java @@ -0,0 +1,10 @@ +package kr.bb.store.domain.store.exception.address; + +import kr.bb.store.exception.CustomException; + +public class SidoNotFoundException extends CustomException { + public static final String MESSAGE = "ํ•ด๋‹น ์‹œ/๋„๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."; + public SidoNotFoundException(){ + super(MESSAGE); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/facade/StoreFacade.java b/src/main/java/kr/bb/store/domain/store/facade/StoreFacade.java new file mode 100644 index 0000000..c31f3cf --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/facade/StoreFacade.java @@ -0,0 +1,214 @@ +package kr.bb.store.domain.store.facade; + +import bloomingblooms.domain.flower.FlowerDto; +import bloomingblooms.domain.notification.order.OrderType; +import bloomingblooms.domain.order.ValidatePolicyDto; +import bloomingblooms.domain.store.StoreAverageDto; +import bloomingblooms.domain.store.StoreInfoDto; +import bloomingblooms.domain.store.StoreNameAndAddressDto; +import bloomingblooms.domain.store.StorePolicy; +import bloomingblooms.domain.wishlist.likes.LikedStoreInfoResponse; +import bloomingblooms.dto.command.UpdateSettlementCommand; +import bloomingblooms.dto.response.SettlementStoreInfoResponse; +import kr.bb.store.client.ProductFeignClient; +import kr.bb.store.client.StoreLikeFeignClient; +import kr.bb.store.client.StoreSubscriptionFeignClient; +import kr.bb.store.domain.coupon.service.CouponService; +import kr.bb.store.domain.store.controller.request.SortType; +import kr.bb.store.domain.store.controller.request.StoreCreateRequest; +import kr.bb.store.domain.store.controller.request.StoreInfoEditRequest; +import kr.bb.store.domain.store.controller.response.*; +import kr.bb.store.domain.store.dto.DeliveryPolicyDto; +import kr.bb.store.domain.store.dto.GugunDto; +import kr.bb.store.domain.store.dto.SidoDto; +import kr.bb.store.domain.store.dto.StoreForAdminDto; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.service.StoreService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.kafka.annotation.KafkaListener; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Component +@RequiredArgsConstructor +public class StoreFacade { + private final StoreService storeService; + private final CouponService couponService; + private final ProductFeignClient productFeignClient; + private final StoreLikeFeignClient storeLikeFeignClient; + private final StoreSubscriptionFeignClient storeSubscriptionFeignClient; + + @KafkaListener(topics = "store-average-rating-update", groupId = "average-rating") + @CacheEvict(cacheNames = "store-list-with-paging", allEntries = true) + public void updateAverageRating(StoreAverageDto storeAverageDto) { + storeService.updateAverageRating(storeAverageDto.getAverage()); + log.info("stores averageRating updated"); + } + + @KafkaListener(topics = "settlement", groupId = "settlement") + @CacheEvict(cacheNames = "store-list-with-paging", allEntries = true) + public void updateMonthlySalesRevenue(UpdateSettlementCommand updateSettlementCommand) { + storeService.updateMonthlySalesRevenue(updateSettlementCommand.getDtoList()); + log.info("stores monthlySalesRevenue updated"); + } + + @CacheEvict(cacheNames = "store-list-with-paging", allEntries = true) + public Long createStore(Long userId, StoreCreateRequest storeCreateRequest) { + List flowers = productFeignClient.getFlowers().getData(); + return storeService.createStore(userId, storeCreateRequest, flowers); + } + + @CacheEvict(cacheNames = "store-list-with-paging", allEntries = true) + public void editStoreInfo(Long storeId, StoreInfoEditRequest storeInfoEditRequest) { + storeService.editStoreInfo(storeId, storeInfoEditRequest); + log.info("info of store {} edited", storeId); + } + + public SimpleStorePagingResponse getStoresWithLikes(Long userId, Pageable pageable) { + Page storePages = storeService.getStoresWithPaging(pageable); + List storeIds = storePages.getContent() + .stream() + .map(StoreListResponse::getStoreId) + .collect(Collectors.toList()); + + if(isLoginUser(userId)) { + Map storeLikes = storeLikeFeignClient.getStoreLikes(userId, storeIds).getData(); + storePages.getContent().forEach(store -> store.setIsLiked(storeLikes.get(store.getStoreId()))); + } + + return SimpleStorePagingResponse.builder() + .stores(storePages.getContent()) + .totalCnt(storePages.getTotalElements()) + .build(); + } + + public StoreInfoUserResponse getStoreInfoForUser(Long userId, Long storeId) { + String subscriptionProductId = productFeignClient.getSubscriptionProductId(storeId).getData() + .getSubscriptionProductId(); + + if(isLoginUser(userId)) { + Map storeLikes = storeLikeFeignClient.getStoreLikes(userId, List.of(storeId)).getData(); + Map storeSubscriptions = storeSubscriptionFeignClient + .getStoreSubscriptions(userId, List.of(storeId)).getData(); + Boolean isLiked = storeLikes.get(storeId); + Boolean isSubscribed = storeSubscriptions.get(storeId); + return storeService.getStoreInfoForUser(storeId, isLiked, isSubscribed, subscriptionProductId); + } + + return storeService.getStoreInfoForUser(storeId, false, false, subscriptionProductId); + } + + public StoreInfoManagerResponse getStoreInfoForManager(Long storeId) { + return storeService.getStoreInfoForManager(storeId); + } + + public StoreListForMapResponse getNearbyStores(Long userId, Double lat, Double lon, Integer level) { + StoreListForMapResponse nearbyStores = storeService.getNearbyStores(lat, lon, level); + + if(isLoginUser(userId)) { + List storeIds = nearbyStores.getStoreIds(); + Map storeLikes = storeLikeFeignClient.getStoreLikes(userId, storeIds).getData(); + nearbyStores.setLikes(storeLikes); + } + + return nearbyStores; + } + + public StoreListForMapResponse getStoresWithRegion(Long userId, String sidoCode, String gugunCode) { + StoreListForMapResponse storesWithRegion = storeService.getStoresWithRegion(sidoCode, gugunCode); + + if(isLoginUser(userId)) { + List storeIds = storesWithRegion.getStoreIds(); + Map storeLikes = storeLikeFeignClient.getStoreLikes(userId, storeIds).getData(); + storesWithRegion.setLikes(storeLikes); + } + + return storesWithRegion; + } + + public Long getStoreId(Long userId) { + return storeService.getStoreId(userId); + } + + public String getStoreName(Long storeId) { + return storeService.getStoreName(storeId); + } + + public Map getStoreNames(List storeIds) { + return storeService.getStoreNames(storeIds); + } + + public StoreNameAndAddressDto getStoreNameAndAddress(Long storeId) { + return storeService.getStoreNameAndAddress(storeId).toCommonDto(); + } + + public StoreDetailInfoResponse getStoreDetailInfo(Long storeId) { + return storeService.getStoreDetailInfo(storeId); + } + + public StoreInfoDto getStoreInfo(Long userId) { + return storeService.getStoreInfo(userId).toCommonDto(); + } + + public List getAllStoreInfos() { + return storeService.getAllStoreInfos().stream() + .map(kr.bb.store.client.dto.StoreInfoDto::toCommonDto) + .collect(Collectors.toList()); + } + + public void validateForOrder(ValidatePolicyDto validatePolicyDto) { + couponService.validateCouponPrice(validatePolicyDto.getValidatePriceDtos()); + if(!validatePolicyDto.getOrderType().equals(OrderType.PICKUP)) { + storeService.validateDeliveryPrice(validatePolicyDto.getValidatePriceDtos()); + } + } + + public List simpleInfos(List storeIds){ + return storeService.simpleInfos(storeIds).stream() + .map(kr.bb.store.domain.store.controller.response.LikedStoreInfoResponse::toCommonDto) + .collect(Collectors.toList()); + } + + public List storeInfoForSettlement(List storeIds) { + return storeService.storeInfoForSettlement(storeIds); + } + + public DeliveryPolicyDto getDeliveryPolicy(Long storeId) { + return storeService.getDeliveryPolicy(storeId); + } + + public Map getDeliveryPolicies(List storeIds) { + return storeService.getDeliveryPolicies(storeIds); + } + + public StoreForAdminDtoResponse getStoresForAdmin(Pageable pageable, SortType sort, String sidoCode, String gugunCode) { + Page storesForAdmin = storeService.getStoresForAdmin(pageable, sort, sidoCode, gugunCode); + List data = storesForAdmin.getContent() + .stream() + .map(StoreForAdminDto::fromEntity) + .collect(Collectors.toList()); + + return StoreForAdminDtoResponse.of(data, storesForAdmin.getTotalElements()); + } + + public List getAllSido() { + return storeService.getAllSido(); + } + + public List getGuguns(String sidoCode) { + return storeService.getGuguns(sidoCode); + } + + private boolean isLoginUser(Long userId) { + return userId != null; + } + +} diff --git a/src/main/java/kr/bb/store/domain/store/handler/GugunReader.java b/src/main/java/kr/bb/store/domain/store/handler/GugunReader.java new file mode 100644 index 0000000..5aac9bb --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/handler/GugunReader.java @@ -0,0 +1,39 @@ +package kr.bb.store.domain.store.handler; + +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.GugunRepository; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.exception.address.GugunNotFoundException; +import kr.bb.store.domain.store.exception.address.InvalidParentException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@RequiredArgsConstructor +@Component +public class GugunReader { + private final GugunRepository gugunRepository; + + public Gugun readGugunCorrespondingSido(Sido sido, String gugunName) { + Gugun gugun = gugunRepository.findBySidoAndName(sido, gugunName) + .orElseThrow(GugunNotFoundException::new); + if(!gugun.getSido().getCode().equals(sido.getCode())) { + throw new InvalidParentException(); + } + return gugun; + } + + public Gugun readGugunCorrespondingSidoWithCode(Sido sido, String gugunCode) { + Gugun gugun = gugunRepository.findById(gugunCode) + .orElseThrow(GugunNotFoundException::new); + if(!gugun.getSido().getCode().equals(sido.getCode())) { + throw new InvalidParentException(); + } + return gugun; + } + + public List readGuguns(String sidoCode) { + return gugunRepository.findGugunBySidoCode(sidoCode); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/handler/SidoReader.java b/src/main/java/kr/bb/store/domain/store/handler/SidoReader.java new file mode 100644 index 0000000..0ece2ec --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/handler/SidoReader.java @@ -0,0 +1,30 @@ +package kr.bb.store.domain.store.handler; + +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.entity.address.SidoRepository; +import kr.bb.store.domain.store.exception.address.SidoNotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@RequiredArgsConstructor +@Component +public class SidoReader { + private final SidoRepository sidoRepository; + + public Sido readSidoByName(String sidoName) { + return sidoRepository.findByName(sidoName) + .orElseThrow(SidoNotFoundException::new); + + } + public Sido readSido(String sidoCode) { + return sidoRepository.findById(sidoCode) + .orElseThrow(SidoNotFoundException::new); + } + + + public List readAll() { + return sidoRepository.findAll(); + } +} diff --git a/src/main/java/kr/bb/store/domain/store/handler/StoreCreator.java b/src/main/java/kr/bb/store/domain/store/handler/StoreCreator.java new file mode 100644 index 0000000..180aadc --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/handler/StoreCreator.java @@ -0,0 +1,82 @@ +package kr.bb.store.domain.store.handler; + +import kr.bb.store.domain.store.controller.request.StoreCreateRequest; +import kr.bb.store.domain.store.dto.DeliveryPolicyRequestDto; +import kr.bb.store.domain.store.dto.StoreAddressDto; +import kr.bb.store.domain.store.entity.DeliveryPolicy; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.exception.CannotOwnMultipleStoreException; +import kr.bb.store.domain.store.repository.DeliveryPolicyRepository; +import kr.bb.store.domain.store.repository.StoreAddressRepository; +import kr.bb.store.domain.store.repository.StoreRepository; +import kr.bb.store.domain.store.dto.StoreDto; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +@RequiredArgsConstructor +@Component +public class StoreCreator { + private final StoreRepository storeRepository; + private final DeliveryPolicyRepository deliveryPolicyRepository; + private final StoreAddressRepository storeAddressRepository; + + public Store create(Long userId, StoreCreateRequest storeCreateRequest, Sido sido, Gugun gugun) { + Store store = createStore(userId, storeCreateRequest.toStoreRequest()); + createDeliveryPolicy(store,storeCreateRequest.toDeliveryPolicyRequest()); + createStoreAddress(sido,gugun,store,storeCreateRequest.toStoreAddressRequest()); + return store; + } + + public Store createStore(Long userId, StoreDto storeDto) { + if(ownerAlreadyHavingStore(userId)) { + throw new CannotOwnMultipleStoreException(); + } + + Store store = Store.builder() + .storeManagerId(userId) + .storeCode(UUID.randomUUID().toString().substring(0,8)) + .storeName(storeDto.getStoreName()) + .detailInfo(storeDto.getDetailInfo()) + .storeThumbnailImage(storeDto.getStoreThumbnailImage()) + .phoneNumber(storeDto.getPhoneNumber()) + .accountNumber(storeDto.getAccountNumber()) + .bank(storeDto.getBank()) + .build(); + return storeRepository.save(store); + } + + private DeliveryPolicy createDeliveryPolicy(Store store, DeliveryPolicyRequestDto deliveryPolicyRequestDto) { + DeliveryPolicy deliveryPolicy = DeliveryPolicy.builder() + .store(store) + .freeDeliveryMinPrice(deliveryPolicyRequestDto.getFreeDeliveryMinPrice()) + .deliveryPrice(deliveryPolicyRequestDto.getDeliveryPrice()) + .build(); + return deliveryPolicyRepository.save(deliveryPolicy); + } + + private StoreAddress createStoreAddress(Sido sido, Gugun gugun, Store store, StoreAddressDto storeAddressDto) { + StoreAddress storeAddress = StoreAddress.builder() + .store(store) + .sido(sido) + .gugun(gugun) + .address(storeAddressDto.getAddress()) + .detailAddress(storeAddressDto.getDetailAddress()) + .zipCode(storeAddressDto.getZipCode()) + .lat(storeAddressDto.getLat()) + .lon(storeAddressDto.getLon()) + .build(); + return storeAddressRepository.save(storeAddress); + } + + + private boolean ownerAlreadyHavingStore(Long userId) { + return storeRepository.findByStoreManagerId(userId).isPresent(); + } +} + + diff --git a/src/main/java/kr/bb/store/domain/store/handler/StoreManager.java b/src/main/java/kr/bb/store/domain/store/handler/StoreManager.java new file mode 100644 index 0000000..04b96da --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/handler/StoreManager.java @@ -0,0 +1,56 @@ +package kr.bb.store.domain.store.handler; + +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.GugunRepository; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.entity.address.SidoRepository; +import kr.bb.store.domain.store.controller.request.StoreInfoEditRequest; +import kr.bb.store.domain.store.entity.DeliveryPolicy; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import kr.bb.store.domain.store.exception.DeliveryPolicyNotFoundException; +import kr.bb.store.domain.store.exception.StoreAddressNotFoundException; +import kr.bb.store.domain.store.exception.StoreNotFoundException; +import kr.bb.store.domain.store.exception.address.GugunNotFoundException; +import kr.bb.store.domain.store.exception.address.SidoNotFoundException; +import kr.bb.store.domain.store.repository.DeliveryPolicyRepository; +import kr.bb.store.domain.store.repository.StoreAddressRepository; +import kr.bb.store.domain.store.repository.StoreRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class StoreManager { + + public void edit(Store store, StoreAddress storeAddress, DeliveryPolicy deliveryPolicy, + Sido sido, Gugun gugun, StoreInfoEditRequest storeInfoEditRequest) { + + store.update( + storeInfoEditRequest.getStoreName(), + storeInfoEditRequest.getDetailInfo(), + storeInfoEditRequest.getStoreThumbnailImage(), + storeInfoEditRequest.getPhoneNumber(), + storeInfoEditRequest.getAccountNumber(), + storeInfoEditRequest.getBank() + ); + + storeAddress.update( + sido, + gugun, + storeInfoEditRequest.getAddress(), + storeInfoEditRequest.getDetailAddress(), + storeInfoEditRequest.getZipCode(), + storeInfoEditRequest.getLat(), + storeInfoEditRequest.getLon() + ); + + + deliveryPolicy.update( + storeInfoEditRequest.getDeliveryPrice(), + storeInfoEditRequest.getFreeDeliveryMinPrice() + ); + + } + +} diff --git a/src/main/java/kr/bb/store/domain/store/handler/StoreReader.java b/src/main/java/kr/bb/store/domain/store/handler/StoreReader.java new file mode 100644 index 0000000..b9d295a --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/handler/StoreReader.java @@ -0,0 +1,159 @@ +package kr.bb.store.domain.store.handler; + +import kr.bb.store.client.dto.StoreInfoDto; +import kr.bb.store.domain.store.controller.response.*; +import kr.bb.store.domain.store.entity.DeliveryPolicy; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.exception.DeliveryPolicyNotFoundException; +import kr.bb.store.domain.store.exception.StoreAddressNotFoundException; +import kr.bb.store.domain.store.exception.StoreNotFoundException; +import kr.bb.store.domain.store.repository.DeliveryPolicyRepository; +import kr.bb.store.domain.store.repository.StoreAddressRepository; +import kr.bb.store.domain.store.repository.StoreRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + + +@RequiredArgsConstructor +@Component +public class StoreReader { + private final StoreRepository storeRepository; + private final StoreAddressRepository storeAddressRepository; + private final DeliveryPolicyRepository deliveryPolicyRepository; + + + public Store findStoreById(Long storeId) { + return storeRepository.findById(storeId).orElseThrow(StoreAddressNotFoundException::new); + } + + public List findStoresByIds(List storeIds) { + return storeRepository.findAllById(storeIds); + } + + public StoreAddress findStoreAddressByStoreId(Long storeId) { + return storeAddressRepository.findByStoreId(storeId) + .orElseThrow(StoreAddressNotFoundException::new); + } + + public List findStoreAddressesByStoreIds(List storeIds) { + return storeAddressRepository.findAllById(storeIds); + } + + public DeliveryPolicy findDeliveryPolicyByStoreId(Long storeId) { + return deliveryPolicyRepository.findByStoreId(storeId) + .orElseThrow(DeliveryPolicyNotFoundException::new); + } + + public StoreDetailInfoResponse readDetailInfo(Long storeId) { + Store store = storeRepository.findById(storeId).orElseThrow(StoreNotFoundException::new); + DeliveryPolicy deliveryPolicy = deliveryPolicyRepository.findByStoreId(storeId) + .orElseThrow(DeliveryPolicyNotFoundException::new); + StoreAddress storeAddress = storeAddressRepository.findByStoreId(storeId) + .orElseThrow(StoreAddressNotFoundException::new); + + return StoreDetailInfoResponse.of(store,deliveryPolicy,storeAddress); + } + + public Page readStoresWithPaging(Pageable pageable) { + return storeRepository.getStoresWithPaging(pageable); + } + + public StoreInfoUserResponse readForUser(Long storeId, Boolean isLiked, Boolean isSubscribed, + String subscriptionProductId) { + Store store = storeRepository.findById(storeId).orElseThrow(StoreNotFoundException::new); + StoreAddress storeAddress = storeAddressRepository.findByStoreId(storeId) + .orElseThrow(StoreAddressNotFoundException::new); + return StoreInfoUserResponse.of(store, storeAddress, isLiked, isSubscribed, subscriptionProductId); + } + + public StoreInfoManagerResponse readForManager(Long storeId) { + Store store = storeRepository.findById(storeId).orElseThrow(StoreNotFoundException::new); + StoreAddress storeAddress = storeAddressRepository.findByStoreId(storeId) + .orElseThrow(StoreAddressNotFoundException::new); + return StoreInfoManagerResponse.of(store, storeAddress); + } + + public StoreListForMapResponse getNearbyStores(Double lat, Double lon, Integer level) { + List nearbyStores = storeRepository.getNearbyStores(lat, lon, levelToMeter(level)); + return StoreListForMapResponse.builder() + .stores(nearbyStores) + .build(); + } + + public StoreListForMapResponse getStoresWithRegion(Sido sido, Gugun gugun) { + List storesWithRegion = storeRepository.getStoresWithRegion(sido, gugun); + return StoreListForMapResponse.builder() + .stores(storesWithRegion) + .build(); + } + + public Optional getStoreByUserId(Long userId) { + return storeRepository.findByStoreManagerId(userId); + } + + public Store read(Long storeId) { + return storeRepository.findById(storeId) + .orElseThrow(StoreNotFoundException::new); + } + + public List reads(List storeIds) { + return storeRepository.findAllByIdIn(storeIds); + } + + public StoreInfoDto readInfo(Long storeId) { + Store store = storeRepository.findById(storeId) + .orElseThrow(StoreNotFoundException::new); + return StoreInfoDto.fromEntity(store); + } + + public List readInfos() { + List stores = storeRepository.findAll(); + return stores.stream() + .map(StoreInfoDto::fromEntity) + .collect(Collectors.toList()); + } + + public Page readStoresOrderByCreatedAt(Pageable pageable, Sido sido, Gugun gugun) { + return storeRepository.getStoresWithRegionAndPagingOrderByCreatedAt(pageable, sido, gugun); + } + + public Page readStoresOrderByAverageRating(Pageable pageable, Sido sido, Gugun gugun) { + return storeRepository.getStoresWithRegionAndPagingOrderByAverageRating(pageable, sido, gugun); + } + + public Page readStoresOrderByMonthlySalesRevenue(Pageable pageable, Sido sido, Gugun gugun) { + return storeRepository.getStoresWithReginAndPagingOrderByMonthlySalesRevenue(pageable, sido, gugun); + } + + public StoreAddress readAddress(Long storeId) { + return storeAddressRepository.findByStoreId(storeId) + .orElseThrow(StoreNotFoundException::new); + } + + private Double levelToMeter(int level) { + switch (level) { + case 1 : + return 150d; + case 2 : + return 250d; + case 3 : + return 500d; + case 4 : + return 1000d; + case 5 : + return 2000d; + default: + throw new IllegalArgumentException("์ •์˜๋˜์ง€ ์•Š์€ ๋ ˆ๋ฒจ์ž…๋‹ˆ๋‹ค"); + } + } + +} diff --git a/src/main/java/kr/bb/store/domain/store/repository/DeliveryPolicyRepository.java b/src/main/java/kr/bb/store/domain/store/repository/DeliveryPolicyRepository.java new file mode 100644 index 0000000..c943f79 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/repository/DeliveryPolicyRepository.java @@ -0,0 +1,10 @@ +package kr.bb.store.domain.store.repository; + +import kr.bb.store.domain.store.entity.DeliveryPolicy; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface DeliveryPolicyRepository extends JpaRepository { + Optional findByStoreId(Long storeId); +} diff --git a/src/main/java/kr/bb/store/domain/store/repository/StoreAddressRepository.java b/src/main/java/kr/bb/store/domain/store/repository/StoreAddressRepository.java new file mode 100644 index 0000000..8c9b977 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/repository/StoreAddressRepository.java @@ -0,0 +1,10 @@ +package kr.bb.store.domain.store.repository; + +import kr.bb.store.domain.store.entity.StoreAddress; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.Optional; + +public interface StoreAddressRepository extends JpaRepository { + Optional findByStoreId(Long storeId); +} diff --git a/src/main/java/kr/bb/store/domain/store/repository/StoreRepository.java b/src/main/java/kr/bb/store/domain/store/repository/StoreRepository.java new file mode 100644 index 0000000..b2a07f0 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/repository/StoreRepository.java @@ -0,0 +1,14 @@ +package kr.bb.store.domain.store.repository; + +import kr.bb.store.domain.store.entity.Store; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface StoreRepository extends JpaRepository,StoreRepositoryCustom { + Optional findByStoreManagerId(Long userId); + + List findAllByIdIn(List storeIds); + +} diff --git a/src/main/java/kr/bb/store/domain/store/repository/StoreRepositoryCustom.java b/src/main/java/kr/bb/store/domain/store/repository/StoreRepositoryCustom.java new file mode 100644 index 0000000..50a18d4 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/repository/StoreRepositoryCustom.java @@ -0,0 +1,26 @@ +package kr.bb.store.domain.store.repository; + +import kr.bb.store.domain.store.controller.response.StoreListResponse; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.controller.response.StoreForMapResponse; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +public interface StoreRepositoryCustom { + Page getStoresWithPaging(Pageable pageable); + + List getNearbyStores(double lat, double lon, double radius); + + List getStoresWithRegion(Sido sido, Gugun gugun); + + Page getStoresWithRegionAndPagingOrderByCreatedAt(Pageable pageable, Sido sido, Gugun gugun); + + Page getStoresWithRegionAndPagingOrderByAverageRating(Pageable pageable, Sido sido, Gugun gugun); + + Page getStoresWithReginAndPagingOrderByMonthlySalesRevenue(Pageable pageable, Sido sido, Gugun gugun); + +} diff --git a/src/main/java/kr/bb/store/domain/store/repository/StoreRepositoryCustomImpl.java b/src/main/java/kr/bb/store/domain/store/repository/StoreRepositoryCustomImpl.java new file mode 100644 index 0000000..e15d3f9 --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/repository/StoreRepositoryCustomImpl.java @@ -0,0 +1,217 @@ +package kr.bb.store.domain.store.repository; + +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.core.types.dsl.Expressions; +import com.querydsl.jpa.impl.JPAQueryFactory; +import kr.bb.store.domain.store.controller.response.QStoreForMapResponse; +import kr.bb.store.domain.store.controller.response.QStoreListResponse; +import kr.bb.store.domain.store.controller.response.StoreForMapResponse; +import kr.bb.store.domain.store.controller.response.StoreListResponse; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.Sido; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import javax.persistence.EntityManager; +import java.util.List; + +import static kr.bb.store.domain.store.entity.QStore.store; +import static kr.bb.store.domain.store.entity.QStoreAddress.storeAddress; + +public class StoreRepositoryCustomImpl implements StoreRepositoryCustom{ + private final JPAQueryFactory queryFactory; + + public StoreRepositoryCustomImpl(EntityManager em) { + this.queryFactory = new JPAQueryFactory(em); + } + + @Override + public Page getStoresWithPaging(Pageable pageable) { + List contents = queryFactory.select(new QStoreListResponse( + store.id, + store.storeThumbnailImage, + store.storeName, + store.detailInfo, + store.averageRating, + Expressions.asBoolean(false), + storeAddress.address, + storeAddress.detailAddress + )) + .from(store) + .leftJoin(storeAddress) + .on(store.id.eq(storeAddress.id)) + .where( + store.isDeleted.isFalse() + ) + .orderBy(store.averageRating.desc()) + .limit(pageable.getPageSize()) + .offset(pageable.getOffset()) + .fetch(); + Long count = queryFactory + .select(store.id.count()) + .from(store) + .where( + store.isDeleted.isFalse() + ) + .fetchOne(); + return new PageImpl<>(contents,pageable,count); + } + + @Override + public List getNearbyStores(double centerLat, double centerLon, double meter) { + return queryFactory.select(new QStoreForMapResponse( + store.id, + store.storeName, + store.detailInfo, + store.storeThumbnailImage, + store.averageRating, + storeAddress.lat, + storeAddress.lon, + storeAddress.address, + storeAddress.detailAddress + )) + .from(storeAddress) + .leftJoin(storeAddress.store, store) + .where( + withinRadius(centerLat, centerLon, meter), + store.isDeleted.isFalse() + ) + .orderBy(nearbyStoreOrderer(centerLat,centerLon)) + .fetch(); + } + + @Override + public List getStoresWithRegion(Sido sido, Gugun gugun) { + return queryFactory.select(new QStoreForMapResponse( + store.id, + store.storeName, + store.detailInfo, + store.storeThumbnailImage, + store.averageRating, + storeAddress.lat, + storeAddress.lon, + storeAddress.address, + storeAddress.detailAddress + )) + .from(storeAddress) + .leftJoin(storeAddress.store, store) + .where( + storeAddress.sido.eq(sido), + gugun != null ? storeAddress.gugun.eq(gugun) : null, + store.isDeleted.isFalse() + ) + .fetch(); + } + + @Override + public Page getStoresWithRegionAndPagingOrderByCreatedAt(Pageable pageable, Sido sido, Gugun gugun) { + List contents = queryFactory.select(store) + .from(storeAddress) + .leftJoin(storeAddress.store, store) + .where( + storeAddress.sido.eq(sido), + gugun != null ? storeAddress.gugun.eq(gugun) : null, + store.isDeleted.isFalse() + ) + .orderBy(store.createdAt.desc()) + .limit(pageable.getPageSize()) + .offset(pageable.getOffset()) + .fetch(); + + Long count = queryFactory + .select(store.id.count()) + .from(storeAddress) + .leftJoin(storeAddress.store, store) + .where( + storeAddress.sido.eq(sido), + gugun != null ? storeAddress.gugun.eq(gugun) : null, + store.isDeleted.isFalse() + ) + .fetchOne(); + return new PageImpl<>(contents,pageable,count); + } + + @Override + public Page getStoresWithRegionAndPagingOrderByAverageRating(Pageable pageable, Sido sido, Gugun gugun) { + List contents = queryFactory.select(store) + .from(storeAddress) + .leftJoin(storeAddress.store, store) + .where( + storeAddress.sido.eq(sido), + gugun != null ? storeAddress.gugun.eq(gugun) : null, + store.isDeleted.isFalse() + ) + .orderBy(store.averageRating.desc()) + .limit(pageable.getPageSize()) + .offset(pageable.getOffset()) + .fetch(); + + Long count = queryFactory + .select(store.id.count()) + .from(storeAddress) + .leftJoin(storeAddress.store, store) + .where( + storeAddress.sido.eq(sido), + gugun != null ? storeAddress.gugun.eq(gugun) : null, + store.isDeleted.isFalse() + ) + .fetchOne(); + return new PageImpl<>(contents,pageable,count); + } + + @Override + public Page getStoresWithReginAndPagingOrderByMonthlySalesRevenue(Pageable pageable, Sido sido, Gugun gugun) { + List contents = queryFactory.select(store) + .from(storeAddress) + .leftJoin(storeAddress.store, store) + .where( + storeAddress.sido.eq(sido), + gugun != null ? storeAddress.gugun.eq(gugun) : null, + store.isDeleted.isFalse() + ) + .orderBy(store.monthlySalesRevenue.desc()) + .limit(pageable.getPageSize()) + .offset(pageable.getOffset()) + .fetch(); + + Long count = queryFactory + .select(store.id.count()) + .from(storeAddress) + .leftJoin(storeAddress.store, store) + .where( + storeAddress.sido.eq(sido), + gugun != null ? storeAddress.gugun.eq(gugun) : null, + store.isDeleted.isFalse() + ) + .fetchOne(); + return new PageImpl<>(contents,pageable,count); + } + + private OrderSpecifier nearbyStoreOrderer(double centerLat, double centerLon) { + return storeAddress.lat.abs().subtract(centerLat) + .add(storeAddress.lon.abs().subtract(centerLon)) + .asc(); + } + + private BooleanExpression withinRadius(double centerLat, double centerLon, double meter) { + return storeAddress.lat.between(centerLat - metersToLatitude(meter), centerLat + metersToLatitude(meter)) + .and(storeAddress.lon.between(centerLon - metersToLongitude(centerLat, meter), centerLon + metersToLongitude(centerLat, meter))); + } + + private static double metersToLatitude(double meters) { + // ์œ„๋„ 1๋„๋‹น ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ (Haversine ๊ณต์‹ ์‚ฌ์šฉ) + double latDiff = meters / 6371000.0; // ์ง€๊ตฌ ๋ฐ˜์ง€๋ฆ„: 6371km (๋ฏธํ„ฐ ๋‹จ์œ„๋กœ ๋ณ€ํ™˜) + return Math.toDegrees(latDiff); + } + + private static double metersToLongitude(double centerLat, double meters) { + // ๊ฒฝ๋„ 1๋„๋‹น ๊ฑฐ๋ฆฌ ๊ณ„์‚ฐ (Haversine ๊ณต์‹ ์‚ฌ์šฉ) + double latRadians = Math.toRadians(centerLat); + double lonDiff = meters / (6371000.0 * Math.cos(latRadians)); // ์ง€๊ตฌ ๋ฐ˜์ง€๋ฆ„: 6371km (๋ฏธํ„ฐ ๋‹จ์œ„๋กœ ๋ณ€ํ™˜) + return Math.toDegrees(lonDiff); + } + +} diff --git a/src/main/java/kr/bb/store/domain/store/service/StoreService.java b/src/main/java/kr/bb/store/domain/store/service/StoreService.java new file mode 100644 index 0000000..1a6288f --- /dev/null +++ b/src/main/java/kr/bb/store/domain/store/service/StoreService.java @@ -0,0 +1,226 @@ +package kr.bb.store.domain.store.service; + +import bloomingblooms.domain.flower.FlowerDto; +import bloomingblooms.domain.order.ValidatePriceDto; +import bloomingblooms.domain.store.StorePolicy; +import bloomingblooms.dto.command.StoreSettlementDto; +import bloomingblooms.dto.response.SettlementStoreInfoResponse; +import kr.bb.store.client.dto.StoreInfoDto; +import kr.bb.store.client.dto.StoreNameAndAddressDto; +import kr.bb.store.domain.cargo.service.CargoService; +import kr.bb.store.domain.store.controller.request.SortType; +import kr.bb.store.domain.store.controller.request.StoreCreateRequest; +import kr.bb.store.domain.store.controller.request.StoreInfoEditRequest; +import kr.bb.store.domain.store.controller.response.*; +import kr.bb.store.domain.store.dto.DeliveryPolicyDto; +import kr.bb.store.domain.store.dto.GugunDto; +import kr.bb.store.domain.store.dto.SidoDto; +import kr.bb.store.domain.store.entity.DeliveryPolicy; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.exception.DeliveryInconsistencyException; +import kr.bb.store.domain.store.handler.*; +import kr.bb.store.util.RestPage; +import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class StoreService { + private final StoreCreator storeCreator; + private final StoreManager storeManager; + private final StoreReader storeReader; + private final SidoReader sidoReader; + private final GugunReader gugunReader; + private final CargoService cargoService; + + + @Transactional + public Long createStore(Long userId, StoreCreateRequest storeCreateRequest, List flowers) { + Sido sido = sidoReader.readSidoByName(storeCreateRequest.getSido()); + Gugun gugun = gugunReader.readGugunCorrespondingSido(sido, storeCreateRequest.getGugun()); + Store store = storeCreator.create(userId, storeCreateRequest, sido, gugun); + cargoService.createBasicCargo(store, flowers); + return store.getId(); + } + + @Transactional + public void editStoreInfo(Long storeId, StoreInfoEditRequest storeInfoEditRequest) { + Store store = storeReader.findStoreById(storeId); + StoreAddress storeAddress = storeReader.findStoreAddressByStoreId(storeId); + DeliveryPolicy deliveryPolicy = storeReader.findDeliveryPolicyByStoreId(storeId); + Sido sido = sidoReader.readSidoByName(storeInfoEditRequest.getSido()); + Gugun gugun = gugunReader.readGugunCorrespondingSido(sido, storeInfoEditRequest.getGugun()); + storeManager.edit(store, storeAddress, deliveryPolicy, sido, gugun, storeInfoEditRequest); + } + + @Transactional + public void updateAverageRating(Map averageRatings) { + averageRatings.forEach((storeId, averageRating) -> { + Store store = storeReader.read(storeId); + store.updateAverageRating(averageRating); + }); + } + + @Transactional + public void updateMonthlySalesRevenue(List monthlySalesRevenues) { + monthlySalesRevenues.forEach(dto -> { + Store store = storeReader.read(dto.getStoreId()); + store.updateMonthlySalesRevenue(dto.getSettlementAmount()); + }); + } + + public StoreDetailInfoResponse getStoreDetailInfo(Long storeId) { + return storeReader.readDetailInfo(storeId); + } + + // ์Šคํ”„๋ง์˜ ๊ธฐ๋ณธ PageImpl์€ ๊ธฐ๋ณธ์ƒ์„ฑ์ž๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์•„ String์œผ๋กœ ์ €์žฅ๋œ ์บ์‹ฑ ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†์Œ + // RestPage๊ฐ์ฒด๋Š” @JsonCreator๋ฅผ ์‚ฌ์šฉํ•ด ๊ธฐ๋ณธ์ƒ์„ฑ์ž๊ฐ€ ์•„๋‹Œ ์ธ์ž๊ฐ€ ์žˆ๋Š” ์ƒ์„ฑ์ž๋กœ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”๋ฅผ ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ–ˆ๋‹ค + @Cacheable(key = "#pageable.pageNumber + '::' + #pageable.pageSize", cacheNames = "store-list-with-paging") + public RestPage getStoresWithPaging(Pageable pageable) { + return new RestPage<>(storeReader.readStoresWithPaging(pageable)); + } + + public StoreInfoUserResponse getStoreInfoForUser(Long storeId, Boolean isLiked, Boolean isSubscribed, String subscriptionProductId) { + return storeReader.readForUser(storeId, isLiked, isSubscribed, subscriptionProductId); + } + + public StoreInfoManagerResponse getStoreInfoForManager(Long storeId) { + return storeReader.readForManager(storeId); + } + + public StoreListForMapResponse getNearbyStores(Double lat, Double lon, Integer level) { + return storeReader.getNearbyStores(lat, lon, level); + } + + public StoreListForMapResponse getStoresWithRegion(String sidoCode, String gugunCode) { + Sido sido = sidoReader.readSido(sidoCode); + Gugun gugun = "".equals(gugunCode) ? null : gugunReader.readGugunCorrespondingSidoWithCode(sido, gugunCode); + return storeReader.getStoresWithRegion(sido, gugun); + } + + public Long getStoreId(Long userId) { + Optional storeByUserId = storeReader.getStoreByUserId(userId); + return storeByUserId.map(Store::getId).orElse(null); + } + + public String getStoreName(Long storeId) { + Store store = storeReader.read(storeId); + return store.getStoreName(); + } + + public Map getStoreNames(List storeIds) { + List stores = storeReader.reads(storeIds); + return stores.stream() + .collect(Collectors.toMap(Store::getId, Store::getStoreName)); + } + + public StoreNameAndAddressDto getStoreNameAndAddress(Long storeId) { + Store store = storeReader.read(storeId); + StoreAddress storeAddress = storeReader.readAddress(storeId); + return StoreNameAndAddressDto.of(store, storeAddress); + } + + public StoreInfoDto getStoreInfo(Long storeId) { + return storeReader.readInfo(storeId); + } + + public List getAllStoreInfos() { + return storeReader.readInfos(); + } + + public void validateDeliveryPrice(List validatePriceDtos) { + validatePriceDtos.forEach(dto -> { + DeliveryPolicy deliveryPolicy = storeReader.findDeliveryPolicyByStoreId(dto.getStoreId()); + Long receivedPaymentPrice = dto.getActualAmount(); + Long receivedDeliveryPrice = dto.getDeliveryCost(); + if(!deliveryPolicy.isRightDeliveryPrice(receivedPaymentPrice, receivedDeliveryPrice)) { + throw new DeliveryInconsistencyException(); + } + }); + } + + public List simpleInfos(List storeIds) { + return storeReader.findStoresByIds(storeIds).stream() + .map(LikedStoreInfoResponse::fromEntity) + .collect(Collectors.toList()); + } + + public List storeInfoForSettlement(List storeIds) { + Map stores = storeReader.findStoresByIds(storeIds).stream() + .collect(Collectors.toMap(Store::getId, store -> store)); + Map storeAddresses = storeReader.findStoreAddressesByStoreIds(storeIds).stream() + .collect(Collectors.toMap(storeAddress -> storeAddress.getStore().getId(), storeAddress -> storeAddress)); + + return storeIds.stream() + .map(id -> SettlementStoreInfoResponse.builder() + .storeId(id) + .storeName(stores.get(id).getStoreName()) + .bankName(stores.get(id).getBank()) + .accountNumber(stores.get(id).getAccountNumber()) + .sido(storeAddresses.get(id).getSido().getName()) + .gugun(storeAddresses.get(id).getGugun().getName()) + .build() + ).collect(Collectors.toList()); + } + + public DeliveryPolicyDto getDeliveryPolicy(Long storeId) { + DeliveryPolicy deliveryPolicy = storeReader.findDeliveryPolicyByStoreId(storeId); + return DeliveryPolicyDto.fromEntity(deliveryPolicy); + } + + public Map getDeliveryPolicies(List storeIds) { + return storeIds.stream().collect(Collectors.toMap(storeId -> storeId, + storeId -> { + DeliveryPolicy deliveryPolicy = storeReader.findDeliveryPolicyByStoreId(storeId); + return StorePolicy.builder() + .storeName(deliveryPolicy.getStore().getStoreName()) + .deliveryCost(deliveryPolicy.getDeliveryPrice()) + .freeDeliveryMinCost(deliveryPolicy.getFreeDeliveryMinPrice()) + .build(); + } + )); + } + + public Page getStoresForAdmin(Pageable pageable, SortType sort, String sidoCode, String gugunCode) { + Sido sido = sidoReader.readSido(sidoCode); + Gugun gugun = "".equals(gugunCode) ? null : gugunReader.readGugunCorrespondingSidoWithCode(sido, gugunCode); + sort = (sort == null) ? SortType.DATE : sort; + + switch (sort) { + case RATE: + return storeReader.readStoresOrderByAverageRating(pageable, sido, gugun); + case AMOUNT: + return storeReader.readStoresOrderByMonthlySalesRevenue(pageable, sido, gugun); + default: + return storeReader.readStoresOrderByCreatedAt(pageable, sido, gugun); + } + } + + public List getAllSido() { + return sidoReader.readAll() + .stream() + .map(SidoDto::fromEntity) + .collect(Collectors.toList()); + } + + public List getGuguns(String sidoCode) { + return gugunReader.readGuguns(sidoCode) + .stream() + .map(GugunDto::fromEntity) + .collect(Collectors.toList()); + } + +} diff --git a/src/main/java/kr/bb/store/exception/CustomException.java b/src/main/java/kr/bb/store/exception/CustomException.java new file mode 100644 index 0000000..7d9c536 --- /dev/null +++ b/src/main/java/kr/bb/store/exception/CustomException.java @@ -0,0 +1,11 @@ +package kr.bb.store.exception; + +public class CustomException extends RuntimeException { + public CustomException() { + super(); + } + + public CustomException(String message) { + super(message); + } +} diff --git a/src/main/java/kr/bb/store/exception/NonPropagatingException.java b/src/main/java/kr/bb/store/exception/NonPropagatingException.java new file mode 100644 index 0000000..5a90172 --- /dev/null +++ b/src/main/java/kr/bb/store/exception/NonPropagatingException.java @@ -0,0 +1,8 @@ +package kr.bb.store.exception; + +public class NonPropagatingException extends CustomException { + + public NonPropagatingException(String message) { + super(message); + } +} diff --git a/src/main/java/kr/bb/store/exception/advice/ControllerAdvice.java b/src/main/java/kr/bb/store/exception/advice/ControllerAdvice.java new file mode 100644 index 0000000..36676c5 --- /dev/null +++ b/src/main/java/kr/bb/store/exception/advice/ControllerAdvice.java @@ -0,0 +1,25 @@ +package kr.bb.store.exception.advice; + +import bloomingblooms.response.CommonResponse; +import kr.bb.store.exception.CustomException; +import kr.bb.store.exception.NonPropagatingException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Slf4j +@RestControllerAdvice +public class ControllerAdvice { + + @ExceptionHandler(NonPropagatingException.class) + public void nonPropagatingException(NonPropagatingException e) { + log.warn("this error [{}] will not be thrown to user. rest logic will work well", e.getMessage()); + } + + @ExceptionHandler(CustomException.class) + public CommonResponse customException(CustomException e) { + log.error(e.getMessage()); + return CommonResponse.fail(e.getMessage(), "CE-01"); + } + +} diff --git a/src/main/java/kr/bb/store/message/AnswerSQSPublisher.java b/src/main/java/kr/bb/store/message/AnswerSQSPublisher.java new file mode 100644 index 0000000..b2fddf7 --- /dev/null +++ b/src/main/java/kr/bb/store/message/AnswerSQSPublisher.java @@ -0,0 +1,46 @@ +package kr.bb.store.message; + +import bloomingblooms.domain.notification.NotificationData; +import bloomingblooms.domain.notification.NotificationKind; +import bloomingblooms.domain.notification.NotificationURL; +import bloomingblooms.domain.notification.PublishNotificationInformation; +import bloomingblooms.domain.notification.question.InqueryResponseNotification; +import com.amazonaws.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class AnswerSQSPublisher { + private final AmazonSQS sqs; + private final ObjectMapper objectMapper; + + @Value("${cloud.aws.sqs.inquery-response-notification-queue.url}") + private String queueUrl; + + public void publish(Long userId, String phoneNumber) { + try { + InqueryResponseNotification inqueryResponseNotification = InqueryResponseNotification.builder() + .userId(userId) + .phoneNumber(phoneNumber) + .build(); + PublishNotificationInformation notificationInformation = + PublishNotificationInformation.getData(NotificationURL.INQUERY, NotificationKind.INQUERY); + NotificationData inqueryResponseNotificationData = + NotificationData.notifyData(inqueryResponseNotification, notificationInformation); + SendMessageRequest sendMessageRequest = new SendMessageRequest( + queueUrl, objectMapper.writeValueAsString(inqueryResponseNotificationData) + ); + sqs.sendMessage(sendMessageRequest); + log.info("answer sqs published to user {}. message kind is : {}", userId, NotificationKind.INQUERY); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/kr/bb/store/message/OrderStatusSQSPublisher.java b/src/main/java/kr/bb/store/message/OrderStatusSQSPublisher.java new file mode 100644 index 0000000..a8f0457 --- /dev/null +++ b/src/main/java/kr/bb/store/message/OrderStatusSQSPublisher.java @@ -0,0 +1,47 @@ +package kr.bb.store.message; + +import bloomingblooms.domain.notification.NotificationData; +import bloomingblooms.domain.notification.NotificationKind; +import bloomingblooms.domain.notification.NotificationURL; +import bloomingblooms.domain.notification.PublishNotificationInformation; +import bloomingblooms.domain.order.OrderStatusNotification; +import com.amazonaws.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OrderStatusSQSPublisher { + private final AmazonSQS sqs; + private final ObjectMapper objectMapper; + + @Value("${cloud.aws.sqs.new-order-status-queue.url}") + private String queueUrl; + + public void publish(Long userId, String phoneNumber, NotificationKind notificationKind) { + try { + OrderStatusNotification orderStatusNotification = OrderStatusNotification.builder() + .userId(userId) + .phoneNumber(phoneNumber) + .build(); + PublishNotificationInformation notificationInformation = + PublishNotificationInformation.getData(NotificationURL.ORDER_FAIL, notificationKind); + NotificationData orderStatusNotificationData = + NotificationData.notifyData(orderStatusNotification, notificationInformation); + SendMessageRequest sendMessageRequest = new SendMessageRequest( + queueUrl, objectMapper.writeValueAsString(orderStatusNotificationData) + ); + sqs.sendMessage(sendMessageRequest); + log.info("orderStatus Change sqs published to user {}. message kind is : {}", userId, notificationKind); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} + diff --git a/src/main/java/kr/bb/store/message/OutOfStockSQSPublisher.java b/src/main/java/kr/bb/store/message/OutOfStockSQSPublisher.java new file mode 100644 index 0000000..fe2808c --- /dev/null +++ b/src/main/java/kr/bb/store/message/OutOfStockSQSPublisher.java @@ -0,0 +1,45 @@ +package kr.bb.store.message; + +import bloomingblooms.domain.notification.NotificationData; +import bloomingblooms.domain.notification.NotificationKind; +import bloomingblooms.domain.notification.NotificationURL; +import bloomingblooms.domain.notification.PublishNotificationInformation; +import bloomingblooms.domain.notification.stock.OutOfStockNotification; +import com.amazonaws.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class OutOfStockSQSPublisher { + private final AmazonSQS sqs; + private final ObjectMapper objectMapper; + + @Value("${cloud.aws.sqs.out-of-stock-notification-queue.url}") + private String queueUrl; + + public void publish(Long storeId) { + try { + OutOfStockNotification outOfStockNotification = OutOfStockNotification.builder() + .storeId(storeId) + .build(); + PublishNotificationInformation notificationInformation = + PublishNotificationInformation.getData(NotificationURL.OUT_OF_STOCK, NotificationKind.OUT_OF_STOCK); + NotificationData outOfStockNotificationData = + NotificationData.notifyData(outOfStockNotification, notificationInformation); + SendMessageRequest sendMessageRequest = new SendMessageRequest( + queueUrl, objectMapper.writeValueAsString(outOfStockNotificationData) + ); + sqs.sendMessage(sendMessageRequest); + log.info("outOfStock sqs published to store {}. message kind is : {}", storeId, NotificationKind.OUT_OF_STOCK); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/kr/bb/store/message/QuestionSQSPublisher.java b/src/main/java/kr/bb/store/message/QuestionSQSPublisher.java new file mode 100644 index 0000000..3d862f9 --- /dev/null +++ b/src/main/java/kr/bb/store/message/QuestionSQSPublisher.java @@ -0,0 +1,45 @@ +package kr.bb.store.message; + +import bloomingblooms.domain.notification.NotificationData; +import bloomingblooms.domain.notification.NotificationKind; +import bloomingblooms.domain.notification.NotificationURL; +import bloomingblooms.domain.notification.PublishNotificationInformation; +import bloomingblooms.domain.notification.question.QuestionRegister; +import com.amazonaws.services.sqs.AmazonSQS; +import com.amazonaws.services.sqs.model.SendMessageRequest; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class QuestionSQSPublisher { + private final AmazonSQS sqs; + private final ObjectMapper objectMapper; + + @Value("${cloud.aws.sqs.question-register-notification-queue.url}") + private String queueUrl; + + public void publish(Long storeId) { + try { + QuestionRegister questionRegister = QuestionRegister.builder() + .storeId(storeId) + .build(); + PublishNotificationInformation notificationInformation = + PublishNotificationInformation.getData(NotificationURL.QUESTION, NotificationKind.QUESTION); + NotificationData questionRegisterNotificationData = + NotificationData.notifyData(questionRegister, notificationInformation); + SendMessageRequest sendMessageRequest = new SendMessageRequest( + queueUrl, objectMapper.writeValueAsString(questionRegisterNotificationData) + ); + sqs.sendMessage(sendMessageRequest); + log.info("question sqs published to store {}. message kind is : {}", storeId, NotificationKind.QUESTION); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/kr/bb/store/util/KafkaProcessor.java b/src/main/java/kr/bb/store/util/KafkaProcessor.java new file mode 100644 index 0000000..366e043 --- /dev/null +++ b/src/main/java/kr/bb/store/util/KafkaProcessor.java @@ -0,0 +1,18 @@ +package kr.bb.store.util; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class KafkaProcessor { + private final KafkaTemplate kafkaTemplate; + + public void send(String topicName, T data) { + kafkaTemplate.send(topicName, data); + log.info("kafka send data[{}] to topic[{}]", data, topicName); + } +} diff --git a/src/main/java/kr/bb/store/util/RedisCacheInitializer.java b/src/main/java/kr/bb/store/util/RedisCacheInitializer.java new file mode 100644 index 0000000..503e7ed --- /dev/null +++ b/src/main/java/kr/bb/store/util/RedisCacheInitializer.java @@ -0,0 +1,17 @@ +package kr.bb.store.util; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.ApplicationArguments; +import org.springframework.boot.ApplicationRunner; +import org.springframework.cache.annotation.CacheEvict; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class RedisCacheInitializer implements ApplicationRunner { + @CacheEvict(value = {"store-list-with-paging"}, allEntries = true) + @Override + public void run(ApplicationArguments args) throws Exception { + log.info("store-list Cache Initialized"); + } +} diff --git a/src/main/java/kr/bb/store/util/RedisOperation.java b/src/main/java/kr/bb/store/util/RedisOperation.java new file mode 100644 index 0000000..71ef593 --- /dev/null +++ b/src/main/java/kr/bb/store/util/RedisOperation.java @@ -0,0 +1,51 @@ +package kr.bb.store.util; + +import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.SessionCallback; +import org.springframework.stereotype.Component; + +import java.sql.Date; +import java.time.LocalDate; + +@Component +@RequiredArgsConstructor +public class RedisOperation { + private final RedisTemplate redisTemplate; + + public void add(String key, String value) { + redisTemplate.opsForSet().add(key,value); + } + + public void remove(String key, String value) { + redisTemplate.opsForSet().remove(key,value); + } + + public void setExpr(String key, LocalDate expirationDate) { + redisTemplate.expireAt(key, Date.valueOf(expirationDate)); + } + + public Boolean contains(String key, String value) { + return redisTemplate.opsForSet().isMember(key,value); + } + + public Long count(String key) { + return redisTemplate.opsForSet().size(key); + } + + public Object addAndSetExpr(String key, LocalDate expirationDate) { + return redisTemplate.execute(new SessionCallback<>() { + @Override + public Object execute(RedisOperations operations) throws DataAccessException { + operations.multi(); + add(key, RedisUtils.DUMMY_DATA); + setExpr(key,expirationDate); + return operations.exec(); + } + }); + } + + +} diff --git a/src/main/java/kr/bb/store/util/RedisUtils.java b/src/main/java/kr/bb/store/util/RedisUtils.java new file mode 100644 index 0000000..0756734 --- /dev/null +++ b/src/main/java/kr/bb/store/util/RedisUtils.java @@ -0,0 +1,19 @@ +package kr.bb.store.util; + +import kr.bb.store.domain.coupon.entity.Coupon; + +public class RedisUtils { + public static final String DUMMY_DATA = "DUMMY"; + + public static String makeRedisKey(Coupon coupon) { + return "coupon:" + coupon.getCouponCode() + ":" + coupon.getId(); + } + + public static String makeRedissonKey(Long storeId, Long flowerId) { + return "redisson:" + storeId + ":" + flowerId; + } + + public static String makeRedissonKey(Long storeId) { + return "redisson:" + storeId.toString(); + } +} diff --git a/src/main/java/kr/bb/store/util/RestPage.java b/src/main/java/kr/bb/store/util/RestPage.java new file mode 100644 index 0000000..135a3de --- /dev/null +++ b/src/main/java/kr/bb/store/util/RestPage.java @@ -0,0 +1,28 @@ +package kr.bb.store.util; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.JsonNode; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.PageRequest; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true, value = {"pageable"}) +public class RestPage extends PageImpl { + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public RestPage(@JsonProperty("content") List content, @JsonProperty("number") int number, @JsonProperty("size") int size, + @JsonProperty("totalElements") Long totalElements, @JsonProperty("pageable") JsonNode pageable, @JsonProperty("last") boolean last, + @JsonProperty("totalPages") int totalPages, @JsonProperty("sort") JsonNode sort, @JsonProperty("first") boolean first, + @JsonProperty("numberOfElements") int numberOfElements) { + super(content, PageRequest.of(number, size), totalElements); + } + + public RestPage(Page page) { + super(page.getContent(), page.getPageable(), page.getTotalElements()); + } + +} \ No newline at end of file diff --git a/src/main/java/kr/bb/store/util/luascript/CouponLockExecutor.java b/src/main/java/kr/bb/store/util/luascript/CouponLockExecutor.java new file mode 100644 index 0000000..cd97510 --- /dev/null +++ b/src/main/java/kr/bb/store/util/luascript/CouponLockExecutor.java @@ -0,0 +1,23 @@ +package kr.bb.store.util.luascript; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; +import org.springframework.stereotype.Component; + +import java.util.Collections; + +@Component +@RequiredArgsConstructor +public class CouponLockExecutor implements RedisLuaScriptExecutor{ + + private final RedisTemplate redisTemplate; + + @Override + public Boolean execute(String script, String key, Object... args) { + RedisScript redisScript = new DefaultRedisScript<>(script, Boolean.class); + return redisTemplate.execute(redisScript, Collections.singletonList(key), args[0], String.valueOf(args[1])); + } + +} diff --git a/src/main/java/kr/bb/store/util/luascript/LockScript.java b/src/main/java/kr/bb/store/util/luascript/LockScript.java new file mode 100644 index 0000000..97dfda1 --- /dev/null +++ b/src/main/java/kr/bb/store/util/luascript/LockScript.java @@ -0,0 +1,19 @@ +package kr.bb.store.util.luascript; + +public class LockScript { + /* + * ๋ชจ๋“  ์ฟ ํฐ์€ expirationDate ์„ค์ •์„ ์œ„ํ•ด ์ƒ์„ฑ ์‹œ์ ์— DUMMY_DATA๋ฅผ ๋„ฃ์–ด redis์— ๋“ฑ๋ก๋ฉ๋‹ˆ๋‹ค. + * ๊ฒฐ๊ณผ์ ์œผ๋กœ ํ•˜๋‚˜์˜ ๋ฐ์ดํ„ฐ(DUMMY_DATA)๊ฐ€ ๋” ๋“ค์–ด์žˆ๊ธฐ ๋•Œ๋ฌธ์— ์ด๋ฅผ ๊ณ ๋ คํ•ด '<='๊ฐ€ ์•„๋‹Œ '<'๋กœ ๊ฐœ์ˆ˜๋ฅผ ๋น„๊ตํ•ด์•ผ + * ์ฟ ํฐ์„ ์ƒ์„ฑํ•  ๋•Œ ์„ค์ •ํ•œ limitCnt์ˆ˜ ๋งŒํผ ๋ฐœ๊ธ‰๋ฐ›๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + */ + public static final String script = "local key = KEYS[1]\n" + + "local value = ARGV[1]\n" + + "local limitCnt = tonumber(ARGV[2])\n" + + "local currentCnt = redis.call('SCARD', key)\n" + + "if currentCnt <= limitCnt then\n" + + " redis.call('SADD', key, value)\n" + + " return true\n" + + "else\n" + + " return false\n" + + "end"; +} diff --git a/src/main/java/kr/bb/store/util/luascript/RedisLuaScriptExecutor.java b/src/main/java/kr/bb/store/util/luascript/RedisLuaScriptExecutor.java new file mode 100644 index 0000000..3fd8e6d --- /dev/null +++ b/src/main/java/kr/bb/store/util/luascript/RedisLuaScriptExecutor.java @@ -0,0 +1,5 @@ +package kr.bb.store.util.luascript; + +public interface RedisLuaScriptExecutor { + Object execute(String script, String key, Object... args); +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..1dd441a --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,20 @@ +server: + port: 8700 +spring: + mvc: + pathmatch: + matching-strategy: ant_path_matcher + application: + name: store-service + config: + activate: + on-profile: dev + import: optional:configserver:http://config-service:8888 +management: + endpoints: + web: + exposure: + include: + - "refresh" + - "bus-refresh" + - "health" \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml new file mode 100644 index 0000000..1de3397 --- /dev/null +++ b/src/main/resources/application-local.yml @@ -0,0 +1,39 @@ +server: + port: 8700 +spring: + datasource: + url: "jdbc:mysql://localhost:3306/develop?serverTimezone=Asia/Seoul" + username: "root" + password: "123456" + driver-class-name: com.mysql.cj.jdbc.Driver + application: + name: store-service + config: + activate: + on-profile: local + import: optional:configserver:http://localhost:8888 + kafka: + producer: + bootstrap-servers: localhost:9092 + key-serializer: org.apache.kafka.common.serialization.StringSerializer + value-serializer: org.springframework.kafka.support.serializer.JsonSerializer + consumer: + bootstrap-servers: localhost:9092 + key-deserializer: org.apache.kafka.common.serialization.StringDeserializer + value-deserializer: org.springframework.kafka.support.serializer.ErrorHandlingDeserializer + properties: + spring: + deserializer: + value: + delegate: + class: org.springframework.kafka.support.serializer.JsonDeserializer + json: + trusted: + packages: "*" +management: + endpoints: + web: + exposure: + include: + - "refresh" + - "bus-refresh" diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..76f3e5a --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,20 @@ +server: + port: 8700 +spring: + mvc: + pathmatch: + matching-strategy: ant_path_matcher + application: + name: store-service + config: + activate: + on-profile: prod + import: optional:configserver:http://config-service:8888 +management: + endpoints: + web: + exposure: + include: + - "refresh" + - "bus-refresh" + - "health" \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/BasicIntegrationTestEnv.java b/src/test/java/kr/bb/store/domain/BasicIntegrationTestEnv.java new file mode 100644 index 0000000..5fb7825 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/BasicIntegrationTestEnv.java @@ -0,0 +1,21 @@ +package kr.bb.store.domain; + +import org.redisson.api.RedissonClient; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +@ActiveProfiles("test") +public class BasicIntegrationTestEnv { + /* + * @SpringBootTest๋Š” ์‹ค์ œ๋กœ ๋ชจ๋“  ๋นˆ์„ ๋“ฑ๋กํ•จ + * RedisConfig์—์„œ๋Š” RedissonClient๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•˜๋Š” factory method๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๊ณ  ์ด๋ฅผ ๋นˆ์œผ๋กœ ๋“ฑ๋กํ•˜๋ ค ํ•จ + * ํ•˜์ง€๋งŒ TestContainer๋ฅผ ์“ฐ์ง€ ์•Š๋Š” ํ™˜๊ฒฝ์—์„œ๋Š” Redis๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์ง€ ์•Š์•„ ๋นˆ ๋“ฑ๋ก์— ์‹คํŒจํ•จ + * ๊ทธ๋ž˜์„œ RedissonClient๋งŒ ๋ชจํ‚น์ฒ˜๋ฆฌํ•จ + */ + @MockBean + private RedissonClient redissonClient; +} diff --git a/src/test/java/kr/bb/store/domain/RedisContainerTestEnv.java b/src/test/java/kr/bb/store/domain/RedisContainerTestEnv.java new file mode 100644 index 0000000..ba607c0 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/RedisContainerTestEnv.java @@ -0,0 +1,27 @@ +package kr.bb.store.domain; + +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.GenericContainer; + +@ActiveProfiles("test") +public class RedisContainerTestEnv { + static final String REDIS_IMAGE = "redis:6-alpine"; + static final GenericContainer REDIS_CONTAINER; + + static { + REDIS_CONTAINER = new GenericContainer<>(REDIS_IMAGE) + .withExposedPorts(6379) + .withCommand("--requirepass", "password") + .withReuse(true); + REDIS_CONTAINER.start(); + } + + @DynamicPropertySource + public static void overrideProps(DynamicPropertyRegistry registry){ + registry.add("spring.data.redis.host", REDIS_CONTAINER::getHost); + registry.add("spring.data.redis.port", () -> ""+REDIS_CONTAINER.getMappedPort(6379)); + registry.add("spring.data.redis.password", () -> "password"); + } +} diff --git a/src/test/java/kr/bb/store/domain/cargo/facade/CargoFacadeTest.java b/src/test/java/kr/bb/store/domain/cargo/facade/CargoFacadeTest.java new file mode 100644 index 0000000..de1e597 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/cargo/facade/CargoFacadeTest.java @@ -0,0 +1,235 @@ +package kr.bb.store.domain.cargo.facade; + +import bloomingblooms.domain.flower.StockChangeDto; +import bloomingblooms.domain.flower.StockDto; +import kr.bb.store.domain.RedisContainerTestEnv; +import kr.bb.store.domain.cargo.dto.StockModifyDto; +import kr.bb.store.domain.cargo.entity.FlowerCargo; +import kr.bb.store.domain.cargo.entity.FlowerCargoId; +import kr.bb.store.domain.cargo.repository.FlowerCargoRepository; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionStatus; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.interceptor.DefaultTransactionAttribute; +import org.testcontainers.junit.jupiter.Testcontainers; + +import javax.persistence.EntityManager; +import java.util.List; +import java.util.concurrent.*; +import java.util.stream.LongStream; + +import static org.assertj.core.api.Assertions.assertThat; + +@Testcontainers +@SpringBootTest +class CargoFacadeTest extends RedisContainerTestEnv { + @Autowired + private CargoFacade cargoFacade; + + @Autowired + private FlowerCargoRepository flowerCargoRepository; + + @Autowired + private StoreRepository storeRepository; + + @Autowired + private EntityManager em; + + @Autowired + private PlatformTransactionManager txManager; + + + @DisplayName("๊ฝƒ ์•„์ด๋””์™€ ์ˆ˜๋Ÿ‰์„ ์ž…๋ ฅ๋ฐ›์•„ ์žฌ๊ณ ๋ฅผ ๋ณ€๊ฒฝํ•œ๋‹ค") + @Transactional + @Test + void modifyAllStocks() { + // given + Store store = createStore(); + storeRepository.save(store); + + FlowerCargoId flowerCargoId1 = createFlowerCargoId(store.getId(),2L); + FlowerCargoId flowerCargoId2 = createFlowerCargoId(store.getId(),1L); + FlowerCargoId flowerCargoId3 = createFlowerCargoId(store.getId(),3L); + + FlowerCargo fc1 = createFlowerCargo(flowerCargoId1, 100L, "์žฅ๋ฏธ", store); + FlowerCargo fc2 = createFlowerCargo(flowerCargoId2, 100L, "์žฅ๋ฏธ", store); + FlowerCargo fc3 = createFlowerCargo(flowerCargoId3, 100L, "์žฅ๋ฏธ", store); + + flowerCargoRepository.saveAll(List.of(fc1,fc2,fc3)); + + StockModifyDto s1 = createStockModifyDto(1L, 3L); + StockModifyDto s2 = createStockModifyDto(2L, 6L); + StockModifyDto s3 = createStockModifyDto(3L, 2L); + + // when + cargoFacade.modifyAllStocksWithLock(store.getId(), List.of(s1,s2,s3)); + + em.flush(); + em.clear(); + + List flowerStocks = flowerCargoRepository.findAllByStoreId(store.getId()); + + // then + assertThat(flowerStocks).hasSize(3) + .extracting("stock") + .containsExactlyInAnyOrder( + 3L,6L,2L + ); + } + + + @DisplayName("์žฌ๊ณ  ์ฆ๊ฐ€๋Š” ์—ฌ๋Ÿฌ ์“ฐ๋ ˆ๋“œ์—์„œ ๋™์‹œ์— ์š”์ฒญํ•ด๋„ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค") + @Test + void StockPlusRedissonLockTest() throws InterruptedException, ExecutionException { + // given + final int concurrentRequestCount = 100; + ExecutorService executorService = Executors.newFixedThreadPool(32); + CountDownLatch latch = new CountDownLatch(concurrentRequestCount); + final Long flowerId = 1L; + + Future storeCreate = executorService.submit(() -> { + TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute()); + Store store = createStore(); + storeRepository.save(store); + FlowerCargoId flowerCargoId = createFlowerCargoId(store.getId(), flowerId); + FlowerCargo flowerCargo = createFlowerCargo(flowerCargoId, 0L, "์žฅ๋ฏธ", store); + flowerCargoRepository.save(flowerCargo); + txManager.commit(status); + return store.getId(); + }); + + final Long storeId = storeCreate.get(); + + // when + LongStream.rangeClosed(1L, concurrentRequestCount) + .forEach( idx -> executorService.submit(() -> { + try { + StockDto stockDto = StockDto.builder() + .flowerId(flowerId) + .stock(1L) + .build(); + StockChangeDto stockChangeDto = StockChangeDto + .builder() + .storeId(storeId) + .stockDtos(List.of(stockDto)) + .build(); + + cargoFacade.plusStocksWithLock(List.of(stockChangeDto)); + } catch (Exception ignored) { + } finally { + latch.countDown(); + } + })); + + latch.await(); + FlowerCargoId flowerCargoId = createFlowerCargoId(storeId, flowerId); + FlowerCargo result = flowerCargoRepository.findById(flowerCargoId).get(); + + // then + assertThat(result.getStock()).isEqualTo(concurrentRequestCount); + + } + + @DisplayName("์žฌ๊ณ  ๊ฐ์†Œ๋Š” ์—ฌ๋Ÿฌ ์“ฐ๋ ˆ๋“œ์—์„œ ๋™์‹œ์— ์š”์ฒญํ•ด๋„ ์ •์ƒ์ ์œผ๋กœ ๋™์ž‘ํ•œ๋‹ค") + @Test + void StockMinusRedissonLockTest() throws InterruptedException, ExecutionException { + // given + final int concurrentRequestCount = 100; + ExecutorService executorService = Executors.newFixedThreadPool(32); + CountDownLatch latch = new CountDownLatch(concurrentRequestCount); + final Long flowerId1 = 1L; + final Long flowerId2 = 2L; + + Future storeCreate = executorService.submit(() -> { + TransactionStatus status = txManager.getTransaction(new DefaultTransactionAttribute()); + Store store = createStore(); + storeRepository.save(store); + FlowerCargoId flowerCargoId1 = createFlowerCargoId(store.getId(), flowerId1); + FlowerCargo flowerCargo1 = createFlowerCargo(flowerCargoId1, 100L, "์žฅ๋ฏธ", store); + FlowerCargoId flowerCargoId2 = createFlowerCargoId(store.getId(), flowerId2); + FlowerCargo flowerCargo2 = createFlowerCargo(flowerCargoId2, 100L, "ํ•ด๋ฐ”๋ผ๊ธฐ", store); + flowerCargoRepository.saveAll(List.of(flowerCargo1, flowerCargo2)); + txManager.commit(status); + return store.getId(); + }); + + final Long storeId = storeCreate.get(); + + // when + LongStream.rangeClosed(1L, concurrentRequestCount) + .forEach( idx -> executorService.submit(() -> { + try { + StockDto stockDto1 = StockDto.builder() + .flowerId(flowerId1) + .stock(1L) + .build(); + StockDto stockDto2 = StockDto.builder() + .flowerId(flowerId2) + .stock(1L) + .build(); + StockChangeDto stockChangeDto = StockChangeDto + .builder() + .storeId(storeId) + .stockDtos(List.of(stockDto1,stockDto2)) + .build(); + + cargoFacade.minusStocksWithLock(List.of(stockChangeDto)); + } catch (Exception ignored) { + } finally { + latch.countDown(); + } + })); + + latch.await(); + FlowerCargoId flowerCargoId1 = createFlowerCargoId(storeId, flowerId1); + FlowerCargo result1 = flowerCargoRepository.findById(flowerCargoId1).get(); + + FlowerCargoId flowerCargoId2 = createFlowerCargoId(storeId, flowerId2); + FlowerCargo result2 = flowerCargoRepository.findById(flowerCargoId2).get(); + + // then + assertThat(result1.getStock()).isEqualTo(0); + assertThat(result2.getStock()).isEqualTo(0); + } + + private FlowerCargo createFlowerCargo(FlowerCargoId flowerCargoId, Long stock, String name, Store store) { + return FlowerCargo.builder() + .id(flowerCargoId) + .store(store) + .stock(stock) + .flowerName(name) + .build(); + } + + private Store createStore() { + return Store.builder() + .storeManagerId(1L) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ๋ช…") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + private FlowerCargoId createFlowerCargoId(Long storeId, Long flowerId) { + return FlowerCargoId.builder() + .storeId(storeId) + .flowerId(flowerId) + .build(); + } + + private StockModifyDto createStockModifyDto(Long flowerId, Long stock) { + return StockModifyDto.builder() + .flowerId(flowerId) + .stock(stock) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/cargo/service/CargoServiceTest.java b/src/test/java/kr/bb/store/domain/cargo/service/CargoServiceTest.java new file mode 100644 index 0000000..99d7524 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/cargo/service/CargoServiceTest.java @@ -0,0 +1,292 @@ +package kr.bb.store.domain.cargo.service; + +import bloomingblooms.domain.flower.FlowerDto; +import bloomingblooms.domain.flower.StockChangeDto; +import bloomingblooms.domain.flower.StockDto; +import kr.bb.store.domain.RedisContainerTestEnv; +import kr.bb.store.domain.cargo.controller.response.RemainingStocksResponse; +import kr.bb.store.domain.cargo.dto.StockModifyDto; +import kr.bb.store.domain.cargo.entity.FlowerCargo; +import kr.bb.store.domain.cargo.entity.FlowerCargoId; +import kr.bb.store.domain.cargo.exception.StockCannotBeNegativeException; +import kr.bb.store.domain.cargo.repository.FlowerCargoRepository; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Testcontainers; + +import javax.persistence.EntityManager; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.groups.Tuple.tuple; + +@Testcontainers +@SpringBootTest +class CargoServiceTest extends RedisContainerTestEnv { + + @Autowired + private CargoService cargoService; + + @Autowired + private FlowerCargoRepository flowerCargoRepository; + + @Autowired + private StoreRepository storeRepository; + + @Autowired + private EntityManager em; + + @AfterEach + public void teardown() { + flowerCargoRepository.deleteAllInBatch(); + storeRepository.deleteAllInBatch(); + } + + @DisplayName("๊ฝƒ ์•„์ด๋””์™€ ์ˆ˜๋Ÿ‰์„ ์ž…๋ ฅ๋ฐ›์•„ ์žฌ๊ณ ๋ฅผ ๋ณ€๊ฒฝํ•œ๋‹ค") + @Transactional + @Test + void modifyAllStocks() { + // given + Store store = createStore(); + storeRepository.save(store); + + FlowerCargoId flowerCargoId1 = createFlowerCargoId(store.getId(),2L); + FlowerCargo fc1 = createFlowerCargo(flowerCargoId1, 100L, "์žฅ๋ฏธ", store); + flowerCargoRepository.save(fc1); + + Long modifyStock = 3L; + + StockModifyDto s1 = createStockModifyDto(2L, modifyStock); + + // when + cargoService.modifyAllStocks(store.getId(), List.of(s1)); + + em.flush(); + em.clear(); + + FlowerCargo result = flowerCargoRepository.findAllByStoreId(store.getId()).get(0); + + // then + assertThat(result.getStock()).isEqualTo(modifyStock); + + } + + @DisplayName("ํŠน์ • ๊ฐ€๊ฒŒ, ํŠน์ • ๊ฝƒ์˜ ์žฌ๊ณ ๋ฅผ ๋”ํ•œ๋‹ค") + @Transactional + @Test + void addStock() { + // given + Store store = createStore(); + storeRepository.save(store); + FlowerCargoId flowerCargoId = createFlowerCargoId(store.getId(),1L); + FlowerCargo flowerCargo = createFlowerCargo(flowerCargoId, 100L, "์žฅ๋ฏธ", store); + flowerCargoRepository.save(flowerCargo); + StockDto stockDto = StockDto.builder() + .flowerId(flowerCargoId.getFlowerId()) + .stock(10L) + .build(); + StockChangeDto stockChangeDto = StockChangeDto.builder() + .stockDtos(List.of(stockDto)) + .phoneNumber("010-1111-2222") + .storeId(store.getId()) + .build(); + // when + cargoService.plusStockCounts(List.of(stockChangeDto)); + + em.flush(); + em.clear(); + + FlowerCargo flowerCargoFromDB = flowerCargoRepository.findById(flowerCargoId).get(); + + // then + assertThat(flowerCargoFromDB.getStock()).isEqualTo(110L); + + } + + @DisplayName("ํŠน์ • ๊ฐ€๊ฒŒ, ํŠน์ • ๊ฝƒ์˜ ์žฌ๊ณ ๋ฅผ ์ฐจ๊ฐํ•œ๋‹ค") + @Transactional + @Test + void subtractStock() { + // given + Store store = createStore(); + storeRepository.save(store); + FlowerCargoId flowerCargoId = createFlowerCargoId(store.getId(),1L); + FlowerCargo flowerCargo = createFlowerCargo(flowerCargoId, 100L, "์žฅ๋ฏธ", store); + flowerCargoRepository.save(flowerCargo); + StockDto stockDto = StockDto.builder() + .flowerId(flowerCargoId.getFlowerId()) + .stock(10L) + .build(); + StockChangeDto stockChangeDto = StockChangeDto.builder() + .stockDtos(List.of(stockDto)) + .phoneNumber("010-1111-2222") + .storeId(store.getId()) + .build(); + + // when + cargoService.minusStockCounts(List.of(stockChangeDto)); + + em.flush(); + em.clear(); + + FlowerCargo flowerCargoFromDB = flowerCargoRepository.findById(flowerCargoId).get(); + + // then + assertThat(flowerCargoFromDB.getStock()).isEqualTo(90L); + + } + + @DisplayName("์žฌ๊ณ ๋Š” ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†๋‹ค") + @Transactional + @Test + void stockCannotBeNegative() { + // given + Store store = createStore(); + storeRepository.save(store); + FlowerCargoId flowerCargoId = createFlowerCargoId(store.getId(),1L); + FlowerCargo flowerCargo = createFlowerCargo(flowerCargoId, 100L, "์žฅ๋ฏธ", store); + flowerCargoRepository.save(flowerCargo); + StockDto stockDto = StockDto.builder() + .flowerId(flowerCargoId.getFlowerId()) + .stock(-10000L) + .build(); + StockChangeDto stockChangeDto = StockChangeDto.builder() + .stockDtos(List.of(stockDto)) + .phoneNumber("010-1111-2222") + .storeId(store.getId()) + .build(); + + // when // then + assertThatThrownBy(() -> cargoService.plusStockCounts(List.of(stockChangeDto))) + .isInstanceOf(StockCannotBeNegativeException.class) + .hasMessage("์žฌ๊ณ ๋Š” ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + + } + @DisplayName("์žฌ๊ณ ๋Š” ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†๋‹ค") + @Transactional + @Test + void stockCannotBeNegative2() { + // given + Store store = createStore(); + storeRepository.save(store); + + FlowerCargoId flowerCargoId = createFlowerCargoId(store.getId(),1L); + FlowerCargo fc1 = createFlowerCargo(flowerCargoId, 100L, "์žฅ๋ฏธ", store); + + flowerCargoRepository.saveAll(List.of(fc1)); + + StockModifyDto s1 = createStockModifyDto(1L, -3L); + + // when // then + assertThatThrownBy(() -> cargoService.modifyAllStocks(store.getId(), List.of(s1))) + .isInstanceOf(StockCannotBeNegativeException.class) + .hasMessage("์žฌ๊ณ ๋Š” ์Œ์ˆ˜๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + + } + + @DisplayName("ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ๋ชจ๋“  ์žฌ๊ณ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค") + @Transactional + @Test + void getAllStocks() { + // given + Store store = createStore(); + storeRepository.save(store); + + FlowerCargoId flowerCargoId1 = createFlowerCargoId(store.getId(),2L); + FlowerCargoId flowerCargoId2 = createFlowerCargoId(store.getId(),1L); + FlowerCargoId flowerCargoId3 = createFlowerCargoId(store.getId(),3L); + + FlowerCargo fc1 = createFlowerCargo(flowerCargoId1, 100L, "์žฅ๋ฏธ", store); + FlowerCargo fc2 = createFlowerCargo(flowerCargoId2, 100L, "ํŠค๋ฆฝ", store); + FlowerCargo fc3 = createFlowerCargo(flowerCargoId3, 100L, "๋ฐฑํ•ฉ", store); + + flowerCargoRepository.saveAll(List.of(fc1,fc2,fc3)); + + // when + RemainingStocksResponse stocks = cargoService.getAllStocks(store.getId()); + + // then + assertThat(stocks.getStockInfoDtos()).hasSize(3) + .extracting("flowerId","name") + .containsExactlyInAnyOrder( + tuple(2L,"์žฅ๋ฏธ"), + tuple(1L,"ํŠค๋ฆฝ"), + tuple(3L,"๋ฐฑํ•ฉ") + ); + + } + + @DisplayName("๊ฝƒ ์ข…๋ฅ˜๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ์ˆ˜๋Ÿ‰์ด 0์ธ ๊ธฐ๋ณธ ์ •๋ณด๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + @Transactional + @Test + void createBasicCargo() { + // given + Store store = createStore(); + storeRepository.save(store); + + FlowerDto dto1 = createFlowerDto(1L, "์žฅ๋ฏธ"); + FlowerDto dto2 = createFlowerDto(2L, "๊ตญํ™”"); + List flowers = List.of(dto1,dto2); + + cargoService.createBasicCargo(store,flowers); + em.flush(); + em.clear(); + + // when + List result = flowerCargoRepository.findAllByStoreId(store.getId()); + + // then + assertThat(result).hasSize(2); + assertThat(result.get(0).getStock()).isEqualTo(0); + } + + private FlowerDto createFlowerDto(Long flowerId, String flowerName) { + return FlowerDto.builder() + .flowerId(flowerId) + .flowerName(flowerName) + .build(); + } + private FlowerCargo createFlowerCargo(FlowerCargoId flowerCargoId, Long stock, String name, Store store) { + return FlowerCargo.builder() + .id(flowerCargoId) + .store(store) + .stock(stock) + .flowerName(name) + .build(); + } + + private Store createStore() { + return Store.builder() + .storeManagerId(1L) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ๋ช…") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private FlowerCargoId createFlowerCargoId(Long storeId, Long flowerId) { + return FlowerCargoId.builder() + .storeId(storeId) + .flowerId(flowerId) + .build(); + } + + private StockModifyDto createStockModifyDto(Long flowerId, Long stock) { + return StockModifyDto.builder() + .flowerId(flowerId) + .stock(stock) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/coupon/handler/CouponCreatorTest.java b/src/test/java/kr/bb/store/domain/coupon/handler/CouponCreatorTest.java new file mode 100644 index 0000000..700533e --- /dev/null +++ b/src/test/java/kr/bb/store/domain/coupon/handler/CouponCreatorTest.java @@ -0,0 +1,142 @@ +package kr.bb.store.domain.coupon.handler; + + +import kr.bb.store.domain.RedisContainerTestEnv; +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.exception.InvalidCouponDurationException; +import kr.bb.store.domain.coupon.exception.InvalidCouponStartDateException; +import kr.bb.store.domain.coupon.handler.dto.CouponDto; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Testcontainers +@SpringBootTest +@Transactional +class CouponCreatorTest extends RedisContainerTestEnv { + @Autowired + private CouponCreator couponCreator; + @Autowired + private StoreRepository storeRepository; + + @DisplayName("์ฟ ํฐ ์ •๋ณด๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ์ฟ ํฐ์„ ์ƒ์„ฑํ•œ๋‹ค") + @Test + void createCoupon() { + // given + Store store = createStore(); + storeRepository.save(store); + Integer limitCount = 100; + String couponName = "์ฟ ํฐ๋ช…"; + Long discountPrice = 10000L; + Long minPrice = 100000L; + LocalDate startDate = LocalDate.now(); + LocalDate endDate = LocalDate.now(); + CouponDto couponDto = CouponDto.builder() + .limitCount(limitCount) + .couponName(couponName) + .discountPrice(discountPrice) + .minPrice(minPrice) + .startDate(startDate) + .endDate(endDate) + .build(); + + // when + Coupon coupon = couponCreator.create(store, couponDto); + + // then + assertThat(coupon.getId()).isNotNull(); + + } + + @DisplayName("์ฟ ํฐ ์ข…๋ฃŒ์ผ์€ ์‹œ์ž‘์ผ๋ณด๋‹ค ๋น ๋ฅผ ์ˆ˜ ์—†๋‹ค") + @Test + void endDateMustComesAfterStartDate() { + // given + Store store = createStore(); + storeRepository.save(store); + LocalDate startDate = LocalDate.now(); + LocalDate endDate = LocalDate.now().minusDays(1); + CouponDto couponDto = createCouponDtoWithDate(startDate, endDate); + + // when // then + assertThatThrownBy(() -> + couponCreator.create(store, couponDto)) + .isInstanceOf(InvalidCouponDurationException.class) + .hasMessage("์‹œ์ž‘์ผ๊ณผ ์ข…๋ฃŒ์ผ์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("ํ˜„์žฌ์ผ๋ณด๋‹ค ๋น ๋ฅธ ๋‚ ์งœ๋กœ ์ฟ ํฐ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์—†๋‹ค") + @Test + void startDateMustComesAfterNow() { + // given + Store store = createStore(); + storeRepository.save(store); + LocalDate now = LocalDate.now(); + LocalDate startDate = now.minusDays(1); + LocalDate endDate = now.plusDays(100); + CouponDto couponDto = createCouponDtoWithDate(startDate, endDate); + + // when // then + assertThatThrownBy(() -> + couponCreator.create(store, couponDto)) + .isInstanceOf(InvalidCouponStartDateException.class) + .hasMessage("์‹œ์ž‘์ผ์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + + } + + @DisplayName("์ฟ ํฐ์˜ ์‹œ์ž‘์ผ๊ณผ ์ข…๋ฃŒ์ผ์€ ๋™์ผํ•  ์ˆ˜ ์žˆ๋‹ค") + @Test + void startDateAndEndDateCanEqual() { + // given + Store store = createStore(); + storeRepository.save(store); + LocalDate now = LocalDate.now(); + LocalDate startDate = now; + LocalDate endDate = now; + CouponDto couponDto = createCouponDtoWithDate(startDate, endDate); + + // when + Coupon coupon = couponCreator.create(store, couponDto); + + // then + assertThat(coupon.getId()).isNotNull(); + assertThat(coupon.getStartDate()).isEqualTo(coupon.getEndDate()); + + } + + + private Store createStore() { + return Store.builder() + .storeManagerId(1L) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private CouponDto createCouponDtoWithDate(LocalDate startDate, LocalDate endDate) { + return CouponDto.builder() + .couponName("๋ณ€๊ฒฝ๋œ ์ฟ ํฐ์ด๋ฆ„") + .discountPrice(10_000L) + .minPrice(100_000L) + .limitCount(100) + .startDate(startDate) + .endDate(endDate) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/coupon/handler/CouponIssuerTest.java b/src/test/java/kr/bb/store/domain/coupon/handler/CouponIssuerTest.java new file mode 100644 index 0000000..1de4686 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/coupon/handler/CouponIssuerTest.java @@ -0,0 +1,216 @@ +package kr.bb.store.domain.coupon.handler; + + +import kr.bb.store.domain.RedisContainerTestEnv; +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.entity.IssuedCoupon; +import kr.bb.store.domain.coupon.exception.AlreadyIssuedCouponException; +import kr.bb.store.domain.coupon.exception.CouponOutOfStockException; +import kr.bb.store.domain.coupon.exception.ExpiredCouponException; +import kr.bb.store.domain.coupon.repository.CouponRepository; +import kr.bb.store.domain.coupon.repository.IssuedCouponRepository; +import kr.bb.store.util.RedisOperation; +import kr.bb.store.util.RedisUtils; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Testcontainers; + +import javax.persistence.EntityManager; +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Testcontainers +@SpringBootTest +@Transactional +class CouponIssuerTest extends RedisContainerTestEnv { + @Autowired + private CouponIssuer couponIssuer; + @Autowired + private IssuedCouponRepository issuedCouponRepository; + @Autowired + private StoreRepository storeRepository; + @Autowired + private CouponRepository couponRepository; + @Autowired + private RedisOperation redisOperation; + @Autowired + private EntityManager em; + + + @DisplayName("์‚ฌ์šฉ์ž์—๊ฒŒ ์ฟ ํฐ์„ ๋ฐœ๊ธ‰ํ•ด ์ค€๋‹ค") + @Test + public void issueCoupon(){ + // given + Store store = createStore(); + storeRepository.save(store); + + Integer limitCnt = 100; + Coupon coupon = createCoupon(store, limitCnt); + couponRepository.save(coupon); + + Long userId = 1L; + String nickname = "nick"; + String phoneNumber = "phoneNumber"; + LocalDate issueDate = LocalDate.now(); + + // when + IssuedCoupon issuedCoupon = couponIssuer.issueCoupon(coupon, userId, nickname, phoneNumber, issueDate); + em.flush(); + em.clear(); + + IssuedCoupon result = issuedCouponRepository.findById(issuedCoupon.getId()).get(); + + // then + assertThat(result.getCoupon().getCouponCode()).isEqualTo(coupon.getCouponCode()); + assertThat(result.getIsUsed()).isFalse(); + } + + @DisplayName("์‚ฌ์šฉ ๋‚ ์งœ๊ฐ€ ์ง€๋‚œ ์ฟ ํฐ์€ ๋ฐœ๊ธ‰ํ•  ์ˆ˜ ์—†๋‹ค") + @Test + public void expiredCouponCannotBeIssued() { + // given + Store store = createStore(); + storeRepository.save(store); + + Integer limitCnt = 100; + Coupon coupon = createCoupon(store, limitCnt); + + couponRepository.save(coupon); + + Long userId = 1L; + String nickname = "nick"; + String phoneNumber = "phoneNumber"; + LocalDate expiredDate = LocalDate.now().plusDays(5); + + // when // then + assertThatThrownBy(() -> couponIssuer.issueCoupon(coupon, userId, nickname, phoneNumber, expiredDate)) + .isInstanceOf(ExpiredCouponException.class) + .hasMessage("๊ธฐํ•œ์ด ๋งŒ๋ฃŒ๋œ ์ฟ ํฐ์ž…๋‹ˆ๋‹ค."); + + } + + @DisplayName("์œ ์ €๋Š” ๋™์ผํ•œ ์ฟ ํฐ์„ ์—ฌ๋Ÿฌ๊ฐœ ๋ฐœ๊ธ‰๋ฐ›์„ ์ˆ˜ ์—†๋‹ค") + @Test + public void cannotIssueDuplicateCoupon() { + // given + Store store = createStore(); + storeRepository.save(store); + + Integer limitCnt = 100; + Coupon coupon = createCoupon(store, limitCnt); + couponRepository.save(coupon); + + Long userId = 1L; + String nickname = "nick"; + String phoneNumber = "phoneNumber"; + LocalDate issueDate = LocalDate.now(); + + // when + assertThatThrownBy(() -> { + couponIssuer.issueCoupon(coupon, userId, nickname, phoneNumber, issueDate); + couponIssuer.issueCoupon(coupon, userId, nickname, phoneNumber, issueDate); + }) + .isInstanceOf(AlreadyIssuedCouponException.class) + .hasMessage("์ด๋ฏธ ๋ฐœ๊ธ‰๋ฐ›์€ ์ฟ ํฐ์ž…๋‹ˆ๋‹ค."); + + } + + @DisplayName("๋ฐœ๊ธ‰ ์ˆ˜๋Ÿ‰์„ ์ดˆ๊ณผํ•ด์„œ ์ฟ ํฐ์„ ๋ฐœ๊ธ‰ํ•  ์ˆ˜ ์—†๋‹ค") + @Test + public void CouponCannotBeIssuedExceedingLimitCount() { + // given + Store store = createStore(); + storeRepository.save(store); + + Integer limitCnt = 0; + Coupon coupon = createCoupon(store, limitCnt); + + couponRepository.save(coupon); + + String redisKey = RedisUtils.makeRedisKey(coupon); + redisOperation.addAndSetExpr(redisKey, LocalDate.now().plusDays(1)); + + Long userId = 1L; + String nickname = "nick"; + String phoneNumber = "phoneNumber"; + LocalDate issueDate = LocalDate.now(); + + // when // then + assertThatThrownBy(() -> couponIssuer.issueCoupon(coupon, userId, nickname, phoneNumber, issueDate)) + .isInstanceOf(CouponOutOfStockException.class) + .hasMessage("์ค€๋น„๋œ ์ฟ ํฐ์ด ๋ชจ๋‘ ์†Œ์ง„๋˜์—ˆ์Šต๋‹ˆ๋‹ค."); + + } + + @DisplayName("ํ•ด๋‹น ๊ฐ€๊ฒŒ์—์„œ ๋‹ค์šด๋ฐ›์„ ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ์ฟ ํฐ์„ ๋‹ค์šด๋ฐ›๋Š”๋‹ค") + @Test + public void issueAllCouponsOfStore() { + // given + Store store = createStore(); + storeRepository.save(store); + + Coupon normalCoupon = createCoupon(store, 100); + + Coupon possessedCoupon = createCoupon(store,100); + + Coupon exhaustedCoupon = createCoupon(store, 0); + + List coupons = List.of(normalCoupon,possessedCoupon,exhaustedCoupon); + couponRepository.saveAll(coupons); + + Long userId = 1L; + String nickname = "nick"; + String phoneNumber = "phoneNumber"; + LocalDate issueDate = LocalDate.now(); + + String redisKey = RedisUtils.makeRedisKey(exhaustedCoupon); + redisOperation.addAndSetExpr(redisKey, LocalDate.now().plusDays(1)); + + IssuedCoupon usedCoupon = couponIssuer.issueCoupon(possessedCoupon, userId, nickname, phoneNumber, issueDate); + usedCoupon.use(LocalDate.now()); + + // when + couponIssuer.issuePossibleCoupons(coupons, userId, nickname, phoneNumber, issueDate); + + List usableCouponsOfUser = issuedCouponRepository.findUsableCouponsByUserId(userId); + + // then + assertThat(usableCouponsOfUser).hasSize(1); + } + + + private Store createStore() { + return Store.builder() + .storeManagerId(1L) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private Coupon createCoupon(Store store, int limitCnt) { + return Coupon.builder() + .couponCode("์ฟ ํฐ์ฝ”๋“œ") + .store(store) + .limitCount(limitCnt) + .couponName("์ฟ ํฐ์ด๋ฆ„") + .discountPrice(10000L) + .minPrice(100000L) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/coupon/handler/CouponManagerTest.java b/src/test/java/kr/bb/store/domain/coupon/handler/CouponManagerTest.java new file mode 100644 index 0000000..149ff39 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/coupon/handler/CouponManagerTest.java @@ -0,0 +1,166 @@ +package kr.bb.store.domain.coupon.handler; + +import kr.bb.store.domain.RedisContainerTestEnv; +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.exception.InvalidCouponDurationException; +import kr.bb.store.domain.coupon.exception.InvalidCouponStartDateException; +import kr.bb.store.domain.coupon.handler.dto.CouponDto; +import kr.bb.store.domain.coupon.repository.CouponRepository; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; +import org.testcontainers.junit.jupiter.Testcontainers; + +import javax.persistence.EntityManager; +import java.time.LocalDate; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Testcontainers +@SpringBootTest +@Transactional +class CouponManagerTest extends RedisContainerTestEnv { + @Autowired + private CouponManager couponManager; + @Autowired + private CouponRepository couponRepository; + @Autowired + private StoreRepository storeRepository; + @Autowired + private EntityManager em; + + + @DisplayName("์š”์ฒญ๋ฐ›์€ ๋‚ด์šฉ์œผ๋กœ ์ฟ ํฐ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•œ๋‹ค") + @Test + public void editCoupon() { + // given + Store store = createStore(); + storeRepository.save(store); + Coupon coupon = createCoupon(store); + Coupon savedCoupon = couponRepository.save(coupon); + CouponDto couponDto = CouponDto.builder() + .couponName("๋ณ€๊ฒฝ๋œ ์ฟ ํฐ์ด๋ฆ„") + .discountPrice(99_999L) + .minPrice(999_999L) + .limitCount(999) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + + // when + couponManager.edit(savedCoupon,couponDto); + + em.flush(); + em.clear(); + + Coupon result = couponRepository.findById(savedCoupon.getId()).get(); + assertThat(result.getCouponName()).isEqualTo("๋ณ€๊ฒฝ๋œ ์ฟ ํฐ์ด๋ฆ„"); + assertThat(result.getDiscountPrice()).isEqualTo(99_999L); + assertThat(result.getEndDate()).isEqualTo(LocalDate.now()); + + } + + @DisplayName("์ฟ ํฐ ์ข…๋ฃŒ์ผ์€ ์‹œ์ž‘์ผ๋ณด๋‹ค ๋น ๋ฅธ ๋‚ ์งœ๋กœ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†๋‹ค") + @Test + public void endDateMustComesAfterStartDate() { + // given + Store store = createStore(); + storeRepository.save(store); + Coupon coupon = createCoupon(store); + Coupon savedCoupon = couponRepository.save(coupon); + + LocalDate startDate = LocalDate.now(); + LocalDate endDate = LocalDate.now().minusDays(1); + CouponDto couponDto = CouponDto.builder() + .couponName("๋ณ€๊ฒฝ๋œ ์ฟ ํฐ์ด๋ฆ„") + .discountPrice(99_999L) + .minPrice(999_999L) + .limitCount(999) + .startDate(startDate) + .endDate(endDate) + .build(); + + // when + assertThatThrownBy(() -> + couponManager.edit(savedCoupon, couponDto)) + .isInstanceOf(InvalidCouponDurationException.class) + .hasMessage("์‹œ์ž‘์ผ๊ณผ ์ข…๋ฃŒ์ผ์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("ํ˜„์žฌ์ผ๋ณด๋‹ค ๋น ๋ฅธ ๋‚ ์งœ๋กœ ์ฟ ํฐ์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†๋‹ค") + @Test + void startDateMustComesAfterNow() { + // given + Store store = createStore(); + storeRepository.save(store); + Coupon coupon = createCoupon(store); + Coupon savedCoupon = couponRepository.save(coupon); + + LocalDate now = LocalDate.now(); + LocalDate startDate = now.minusDays(1); + LocalDate endDate = now.plusDays(100); + CouponDto couponDto = CouponDto.builder() + .couponName("๋ณ€๊ฒฝ๋œ ์ฟ ํฐ์ด๋ฆ„") + .discountPrice(99_999L) + .minPrice(999_999L) + .limitCount(999) + .startDate(startDate) + .endDate(endDate) + .build(); + + // when + assertThatThrownBy(() -> + couponManager.edit(savedCoupon, couponDto)) + .isInstanceOf(InvalidCouponStartDateException.class) + .hasMessage("์‹œ์ž‘์ผ์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("์ฟ ํฐ์„ ์‚ญ์ œํ•œ๋‹ค") + @Test + void deleteCoupon() { + // given + Store store = createStore(); + storeRepository.save(store); + Coupon coupon = createCoupon(store); + Coupon savedCoupon = couponRepository.save(coupon); + + // when + couponManager.softDelete(savedCoupon); + + // then + assertThat(savedCoupon.getIsDeleted()).isTrue(); + } + + + + private Store createStore() { + return Store.builder() + .storeManagerId(1L) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private Coupon createCoupon(Store store) { + return Coupon.builder() + .couponCode("์ฟ ํฐ์ฝ”๋“œ") + .store(store) + .limitCount(100) + .couponName("์ฟ ํฐ์ด๋ฆ„") + .discountPrice(10000L) + .minPrice(100000L) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/coupon/handler/CouponReaderTest.java b/src/test/java/kr/bb/store/domain/coupon/handler/CouponReaderTest.java new file mode 100644 index 0000000..54adfe8 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/coupon/handler/CouponReaderTest.java @@ -0,0 +1,435 @@ +package kr.bb.store.domain.coupon.handler; + +import kr.bb.store.domain.RedisContainerTestEnv; +import kr.bb.store.domain.coupon.dto.CouponDto; +import kr.bb.store.domain.coupon.dto.CouponForOwnerDto; +import kr.bb.store.domain.coupon.dto.CouponWithAvailabilityDto; +import kr.bb.store.domain.coupon.dto.CouponWithIssueStatusDto; +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.entity.IssuedCoupon; +import kr.bb.store.domain.coupon.entity.IssuedCouponId; +import kr.bb.store.domain.coupon.repository.CouponRepository; +import kr.bb.store.domain.coupon.repository.IssuedCouponRepository; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import java.time.LocalDate; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Transactional +class CouponReaderTest extends RedisContainerTestEnv { + @Autowired + private CouponReader couponReader; + @Autowired + private StoreRepository storeRepository; + @Autowired + private CouponRepository couponRepository; + @Autowired + private IssuedCouponRepository issuedCouponRepository; + @Autowired + private EntityManager em; + + + + @DisplayName("๊ฐ€๊ฒŒ ์‚ฌ์žฅ์—๊ฒŒ ๋ณด์—ฌ์ค„ ์ฟ ํฐ ์ •๋ณด๋ฅผ ์กฐํšŒํ•œ๋‹ค") + @Test + void readCouponsForOwner() { + // given + Store store = createStore(1L); + storeRepository.save(store); + LocalDate now = LocalDate.now().plusYears(1); + Coupon c1 = createCouponWithDate(store, now, now); + Coupon c2 = createCouponWithDate(store, now.minusDays(5), now.minusDays(1)); + Coupon c3 = createCouponWithDate(store, now.plusDays(1), now.plusDays(5)); + Coupon c4 = createCouponWithDate(store, now.minusDays(5), now.plusDays(5)); + + couponRepository.saveAll(List.of(c1,c2,c3,c4)); + + // when + List result = couponReader.readCouponsForOwner(store.getId(), now); + + // then + assertThat(result).hasSize(3); + + } + @DisplayName("์•„๋ฌด๋„ ๋ฐœ๊ธ‰๋ฐ›์ง€ ์•Š์€ ์ฟ ํฐ์˜ ์ˆ˜๋Ÿ‰์€ ์ฒ˜์Œ ์„ค์ •๊ณผ ๋™์ผํ•˜๋‹ค") + @Test + void couponCountWillEqualIfNobodyIssueTheCoupon() { + // given + Store store = createStore(1L); + storeRepository.save(store); + Coupon c1 = createCoupon(store); + couponRepository.save(c1); + + // when + List result = couponReader.readCouponsForOwner(store.getId(), LocalDate.now()); + + // then + assertThat(result.get(0).getUnusedCount()).isEqualTo(c1.getLimitCount()); + + } + + @DisplayName("์ฟ ํฐ์ด ๋ฐœ๊ธ‰๋˜๋ฉด ๊ฐ€๊ฒŒ์‚ฌ์žฅ์ด ๋ณด๋Š” ์ฟ ํฐ ์ •๋ณด์—๋„ ์ฐจ๊ฐ๋œ ๊ฐœ์ˆ˜๊ฐ€ ์ „๋‹ฌ๋œ๋‹ค") + @Test + void unUsedCountWillDecreaseWhenUserIssueCoupon() { + // given + Store store = createStore(1L); + storeRepository.save(store); + Coupon c1 = createCoupon(store); + couponRepository.save(c1); + + Long userId = 1L; + issuedCouponRepository.save(createIssuedCoupon(c1, userId)); + + // when + List result = couponReader.readCouponsForOwner(store.getId(), LocalDate.now()); + + // then + assertThat(result.get(0).getUnusedCount()).isEqualTo(99); + + } + + @DisplayName("ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ๋ชจ๋“  ์ฟ ํฐ์„ ์œ ์ €์—๊ฒŒ ๋ณด์—ฌ์ค€๋‹ค") + @Test + void readStoreCouponsForUser() { + // given + Store store = createStore(1L); + storeRepository.save(store); + Coupon c1 = createCoupon(store); + Coupon c2 = createCoupon(store); + couponRepository.saveAll(List.of(c1,c2)); + + Long userId = 1L; + issuedCouponRepository.save(createIssuedCoupon(c1, userId)); + + em.flush(); + em.clear(); + + LocalDate now = LocalDate.now(); + + // when + List result = couponReader.readStoreCouponsForUser(userId, store.getId(), now); + + // then + assertThat(result).hasSize(2); + assertThat(result).extracting("isIssued") + .containsExactlyInAnyOrder(true,false); + } + @DisplayName("๋งŒ๋ฃŒ๋œ ์ฟ ํฐ์€ ์œ ์ €์—๊ฒŒ ๋…ธ์ถœ๋˜์ง€ ์•Š๋Š”๋‹ค") + @Test + void expiredCouponWillNotExposedToUser() { + // given + Store store = createStore(1L); + storeRepository.save(store); + Coupon c1 = createCoupon(store); + Coupon c2 = createCoupon(store); + couponRepository.saveAll(List.of(c1,c2)); + + Long userId = 1L; + issuedCouponRepository.save(createIssuedCoupon(c1, userId)); + + em.flush(); + em.clear(); + + LocalDate expirationDate = LocalDate.now().plusDays(5); + + // when + List result = couponReader.readStoreCouponsForUser(userId, store.getId(), expirationDate); + + // then + assertThat(result).hasSize(0); + } + + @DisplayName("ํŠน์ • ๊ฐ€๊ฒŒ์˜ ์ƒํ’ˆ์„ ์ฃผ๋ฌธํ•  ๋•Œ ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์ฟ ํฐ์„ ์กฐํšŒํ•œ๋‹ค") + @Test + void readAvailableCouponsInStore() { + // given + LocalDate now = LocalDate.now(); + Store s1 = createStore(1L); + Store s2 = createStore(1L); + storeRepository.saveAll(List.of(s1,s2)); + + Coupon c1 = createCouponWithDate(s1,now,now.plusDays(5)); + Coupon c2 = createCouponWithDate(s1,now,now.plusDays(5)); + couponRepository.saveAll(List.of(c1,c2)); + + Long totalAmount = Long.MAX_VALUE; + + Long userId = 1L; + // ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฟ ํฐ + issuedCouponRepository.save(createIssuedCoupon(c1, userId)); + + // ์ด๋ฏธ ์‚ฌ์šฉํ•œ ์ฟ ํฐ + IssuedCoupon issuedCoupon = issuedCouponRepository.save(createIssuedCoupon(c2, userId)); + em.flush(); + em.clear(); + IssuedCoupon couponsToUse = issuedCouponRepository.findById(issuedCoupon.getId()).get(); + couponsToUse.use(LocalDate.now()); + + // ํ•ด๋‹น ์ƒํ’ˆ(๊ฐ€๊ฒŒ)๊ณผ ๊ด€๋ จ์—†๋Š” ์ฟ ํฐ + Coupon c3 = createCouponWithDate(s2,now,now.plusDays(5)); + couponRepository.save(c3); + issuedCouponRepository.save(createIssuedCoupon(c3, userId)); + + // ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ์œ ํšจํ•œ ์ฟ ํฐ์ด์ง€๋งŒ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์ง€ ์•Š์€ ์ฟ ํฐ + Coupon c4 = createCouponWithDate(s1,now,now.plusDays(5)); + couponRepository.save(c4); + + // ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ๊ธฐ๊ฐ„์ด ์ง€๋‚œ ์ฟ ํฐ + Coupon c5 = createCouponWithDate(s1,now,now); + couponRepository.save(c5); + + // when + List result = couponReader.readAvailableCouponsInStore(totalAmount, userId, s1.getId(), LocalDate.now().plusDays(2)); + + // then + assertThat(result).hasSize(1); + + } + + @DisplayName("์ฃผ๋ฌธ๊ธˆ์•ก์ด ์ฟ ํฐ์˜ ์ตœ์†Œ์‚ฌ์šฉ๊ธˆ์•ก๋ณด๋‹ค ์ž‘๋‹ค๋ฉด ์‚ฌ์šฉ๋ถˆ๊ฐ€๋กœ ํ‘œ์‹œ๋œ๋‹ค") + @Test + void couponCannotUseWhenTotalAmountLowerThanCouponMinPrice() { + // given + LocalDate now = LocalDate.now(); + Store store = createStore(1L); + storeRepository.save(store); + + Long minPrice = 10_000L; + Coupon coupon = createCouponWithMinPrice(store,minPrice); + couponRepository.save(coupon); + + Long totalAmount = minPrice - 1; + + Long userId = 1L; + // ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฟ ํฐ + issuedCouponRepository.save(createIssuedCoupon(coupon, userId)); + + // when + List result = couponReader.readAvailableCouponsInStore(totalAmount, userId, store.getId(), LocalDate.now()); + + // then + assertThat(result) + .extracting("isAvailable") + .containsExactly(false); + + } + @DisplayName("์ฃผ๋ฌธ๊ธˆ์•ก๊ณผ ์ฟ ํฐ์˜ ์ตœ์†Œ์‚ฌ์šฉ๊ธˆ์•ก์ด ๋™์ผํ•  ๋•Œ๋Š” ์‚ฌ์šฉ๊ฐ€๋Šฅ์œผ๋กœ ํ‘œ์‹œ๋œ๋‹ค") + @Test + void couponCanUseWhenTotalAmountIsEqualToCouponMinPrice() { + // given + LocalDate now = LocalDate.now(); + Store store = createStore(1L); + storeRepository.save(store); + + Long minPrice = 10_000L; + Coupon coupon = createCouponWithMinPrice(store,minPrice); + couponRepository.save(coupon); + + Long totalAmount = minPrice; + + Long userId = 1L; + // ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฟ ํฐ + issuedCouponRepository.save(createIssuedCoupon(coupon, userId)); + + // when + List result = couponReader.readAvailableCouponsInStore(totalAmount, userId, store.getId(), LocalDate.now()); + + // then + assertThat(result) + .extracting("isAvailable") + .containsExactly(true); + + } + + + @DisplayName("๋‚ด๊ฐ€ ๋ณด์œ ํ•œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ชจ๋“  ์ฟ ํฐ์„ ์กฐํšŒํ•œ๋‹ค") + @Test + void readMyValidCoupons() { + // given + LocalDate now = LocalDate.now(); + Store s1 = createStore(1L); + Store s2 = createStore(1L); + storeRepository.saveAll(List.of(s1,s2)); + Coupon c1 = createCouponWithDate(s1,now,now.plusDays(5)); + Coupon c2 = createCoupon(s1); + + couponRepository.saveAll(List.of(c1,c2)); + + Long userId = 1L; + // ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฟ ํฐ + issuedCouponRepository.save(createIssuedCoupon(c1, userId)); + + // ์ด๋ฏธ ์‚ฌ์šฉํ•œ ์ฟ ํฐ + IssuedCoupon issuedCoupon = issuedCouponRepository.save(createIssuedCoupon(c2, userId)); + em.flush(); + em.clear(); + IssuedCoupon couponsToUse = issuedCouponRepository.findById(issuedCoupon.getId()).get(); + couponsToUse.use(LocalDate.now()); + + // ์‚ฌ์šฉ ๊ธฐ๊ฐ„์ด ์ง€๋‚œ ์ฟ ํฐ + Coupon c3 = createCouponWithDate(s2,now,now); + couponRepository.save(c3); + issuedCouponRepository.save(createIssuedCoupon(c3, userId)); + + // ์œ ํšจํ•˜์ง€๋งŒ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์ง€ ์•Š์€ ์ฟ ํฐ + Coupon c4 = createCouponWithDate(s1,now,now.plusDays(5)); + couponRepository.save(c4); + + // when + List result = couponReader.readMyValidCoupons(userId,now.plusDays(1)); + + // then + assertThat(result).hasSize(1); + } + + @DisplayName("๋‚ด๊ฐ€ ๋ณด์œ ํ•œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฟ ํฐ ๊ฐœ์ˆ˜๋ฅผ ์กฐํšŒํ•œ๋‹ค") + @Test + void readMyValidCouponCount() { + // given + LocalDate now = LocalDate.now(); + Store s1 = createStore(1L); + Store s2 = createStore(1L); + storeRepository.saveAll(List.of(s1,s2)); + Coupon c1 = createCouponWithDate(s1,now,now.plusDays(5)); + Coupon c2 = createCoupon(s1); + + couponRepository.saveAll(List.of(c1,c2)); + + Long userId = 1L; + // ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์ฟ ํฐ + issuedCouponRepository.save(createIssuedCoupon(c1, userId)); + + // ์ด๋ฏธ ์‚ฌ์šฉํ•œ ์ฟ ํฐ + IssuedCoupon issuedCoupon = issuedCouponRepository.save(createIssuedCoupon(c2, userId)); + em.flush(); + em.clear(); + IssuedCoupon couponsToUse = issuedCouponRepository.findById(issuedCoupon.getId()).get(); + couponsToUse.use(LocalDate.now()); + + // ์‚ฌ์šฉ ๊ธฐ๊ฐ„์ด ์ง€๋‚œ ์ฟ ํฐ + Coupon c3 = createCouponWithDate(s2,now,now); + couponRepository.save(c3); + issuedCouponRepository.save(createIssuedCoupon(c3, userId)); + + // ์œ ํšจํ•˜์ง€๋งŒ ๋‹ค์šด๋กœ๋“œ ๋ฐ›์ง€ ์•Š์€ ์ฟ ํฐ + Coupon c4 = createCouponWithDate(s1,now,now.plusDays(5)); + couponRepository.save(c4); + + // when + int result = couponReader.readMyValidCouponCount(userId,now.plusDays(1)); + + // then + assertThat(result).isEqualTo(1); + } + + @DisplayName("ํ˜„์žฌ ๊ฐ€๊ฒŒ์˜ ์œ ํšจํ•œ ์ฟ ํฐ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void readStoresAllValidateCoupon() { + // given + LocalDate now = LocalDate.now(); + Store s1 = createStore(1L); + Store s2 = createStore(1L); + storeRepository.saveAll(List.of(s1,s2)); + Long userId = 1L; + + Coupon c1 = createCouponWithDate(s1,now,now.plusDays(5)); // ๋‹ค์šด๋กœ๋“œ ๊ฐ€๋Šฅํ•œ ์ฟ ํฐ + + Coupon c2 = createCouponWithDate(s1,now,now.plusDays(5)); // ๋‹ค์šด๋กœ๋“œ ๋ฐ›์€ ์ฟ ํฐ + Coupon c3 = createCouponWithDate(s1,now,now.plusDays(5)); // ๋‹ค์šด๋กœ๋“œ ๋ฐ›์•„์„œ ์‚ฌ์šฉํ•œ ์ฟ ํฐ + Coupon c4 = createCoupon(s1); // ๋งŒ๋ฃŒ๋œ ์ฟ ํฐ + Coupon c5 = createCouponWithDate(s2,now,now.plusDays(5)); // ๋‹ค๋ฅธ ๊ฐ€๊ฒŒ์˜ ์ฟ ํฐ + couponRepository.saveAll(List.of(c1,c2,c3,c4,c5)); + + issuedCouponRepository.save(createIssuedCoupon(c2, userId)); + + IssuedCoupon issuedCoupon = issuedCouponRepository.save(createIssuedCoupon(c3, userId)); + issuedCoupon.use(now); + + // when + List result = couponReader.readStoresAllValidateCoupon(s1.getId(), now.plusDays(3)); + + // then + assertThat(result).hasSize(3) + .containsExactly(c1,c2,c3); + + + } + + + private IssuedCoupon createIssuedCoupon(Coupon coupon, Long userId) { + return IssuedCoupon.builder() + .id(createIssuedCouponId(coupon.getId(),userId)) + .coupon(coupon) + .build(); + } + + private IssuedCouponId createIssuedCouponId(Long couponId, Long userId) { + return IssuedCouponId.builder() + .couponId(couponId) + .userId(userId) + .build(); + } + + private Store createStore(Long storeOwnerId) { + return Store.builder() + .storeManagerId(storeOwnerId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private Coupon createCoupon(Store store) { + return Coupon.builder() + .couponCode("์ฟ ํฐ์ฝ”๋“œ") + .store(store) + .limitCount(100) + .couponName("์ฟ ํฐ์ด๋ฆ„") + .discountPrice(10000L) + .minPrice(100000L) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + } + + private Coupon createCouponWithDate(Store store, LocalDate startDate, LocalDate endDate) { + return Coupon.builder() + .couponCode("์ฟ ํฐ์ฝ”๋“œ") + .store(store) + .limitCount(100) + .couponName("์ฟ ํฐ์ด๋ฆ„") + .discountPrice(10000L) + .minPrice(100000L) + .startDate(startDate) + .endDate(endDate) + .build(); + } + + private Coupon createCouponWithMinPrice(Store store, Long minPrice) { + return Coupon.builder() + .couponCode("์ฟ ํฐ์ฝ”๋“œ") + .store(store) + .limitCount(100) + .couponName("์ฟ ํฐ์ด๋ฆ„") + .discountPrice(10000L) + .minPrice(minPrice) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/coupon/handler/IssuedCouponReaderTest.java b/src/test/java/kr/bb/store/domain/coupon/handler/IssuedCouponReaderTest.java new file mode 100644 index 0000000..38ad1d4 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/coupon/handler/IssuedCouponReaderTest.java @@ -0,0 +1,91 @@ +package kr.bb.store.domain.coupon.handler; + +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.entity.IssuedCoupon; +import kr.bb.store.domain.coupon.entity.IssuedCouponId; +import kr.bb.store.domain.coupon.repository.CouponRepository; +import kr.bb.store.domain.coupon.repository.IssuedCouponRepository; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.time.LocalDate; +import java.util.List; + +class IssuedCouponReaderTest extends BasicIntegrationTestEnv { + @Autowired + private IssuedCouponReader issuedCouponReader; + @Autowired + private StoreRepository storeRepository; + @Autowired + private CouponRepository couponRepository; + @Autowired + private IssuedCouponRepository issuedCouponRepository; + + @DisplayName("์œ ์ €Id์™€ ์ฟ ํฐId๋กœ ํ•ด๋‹น ์œ ์ €์˜ ์ฟ ํฐ ๋ฐœ๊ธ‰ ์—ฌ๋ถ€๋ฅผ ์กฐ์‚ฌํ•œ๋‹ค") + @Test + void readIssuedCoupon() { + // given + Store store = createStore(1L); + storeRepository.save(store); + Coupon c1 = createCoupon(store); + Coupon c2 = createCoupon(store); + couponRepository.saveAll(List.of(c1,c2)); + Long userId = 1L; + IssuedCoupon issuedCoupon = createIssuedCoupon(c1,userId); + issuedCouponRepository.save(issuedCoupon); + + // when + IssuedCoupon result = issuedCouponReader.read(c1.getId(), userId); + + // then + Assertions.assertThat(result.getId()).isNotNull(); + + } + + + private Coupon createCoupon(Store store) { + return Coupon.builder() + .couponCode("์ฟ ํฐ์ฝ”๋“œ") + .store(store) + .limitCount(100) + .couponName("์ฟ ํฐ์ด๋ฆ„") + .discountPrice(10000L) + .minPrice(100000L) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + } + + private Store createStore(Long storeOwnerId) { + return Store.builder() + .storeManagerId(storeOwnerId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private IssuedCoupon createIssuedCoupon(Coupon coupon, Long userId) { + return IssuedCoupon.builder() + .id(createIssuedCouponId(coupon.getId(),userId)) + .coupon(coupon) + .build(); + } + + private IssuedCouponId createIssuedCouponId(Long couponId, Long userId) { + return IssuedCouponId.builder() + .couponId(couponId) + .userId(userId) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/coupon/service/CouponServiceTest.java b/src/test/java/kr/bb/store/domain/coupon/service/CouponServiceTest.java new file mode 100644 index 0000000..cbaa451 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/coupon/service/CouponServiceTest.java @@ -0,0 +1,386 @@ +package kr.bb.store.domain.coupon.service; + + +import bloomingblooms.domain.order.ValidatePriceDto; +import kr.bb.store.domain.RedisContainerTestEnv; +import kr.bb.store.domain.cargo.repository.FlowerCargoRepository; +import kr.bb.store.domain.coupon.controller.request.CouponEditRequest; +import kr.bb.store.domain.coupon.entity.Coupon; +import kr.bb.store.domain.coupon.entity.IssuedCoupon; +import kr.bb.store.domain.coupon.entity.IssuedCouponId; +import kr.bb.store.domain.coupon.exception.CouponInconsistencyException; +import kr.bb.store.domain.coupon.exception.UnAuthorizedCouponException; +import kr.bb.store.domain.coupon.repository.CouponRepository; +import kr.bb.store.domain.coupon.repository.IssuedCouponRepository; +import kr.bb.store.util.RedisUtils; +import kr.bb.store.util.RedisOperation; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.testcontainers.junit.jupiter.Testcontainers; + +import java.time.LocalDate; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.*; +import java.util.stream.LongStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@Testcontainers +@SpringBootTest +class CouponServiceTest extends RedisContainerTestEnv { + @Autowired + private CouponService couponService; + @Autowired + private CouponRepository couponRepository; + @Autowired + private StoreRepository storeRepository; + @Autowired + private FlowerCargoRepository cargoRepository; + @Autowired + private IssuedCouponRepository issuedCouponRepository; + @Autowired + private RedisOperation redisOperation; + + @AfterEach + void teardown() { + issuedCouponRepository.deleteAllInBatch(); + couponRepository.deleteAllInBatch(); + cargoRepository.deleteAllInBatch(); + storeRepository.deleteAllInBatch(); + } + + @DisplayName("์š”์ฒญ๋ฐ›์€ ๋‚ด์šฉ์œผ๋กœ ์ฟ ํฐ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•œ๋‹ค") + @Test + void editCoupon() { + // given + Store store = createStore(); + storeRepository.save(store); + Coupon coupon = couponCreator(store); + Coupon savedCoupon = couponRepository.save(coupon); + CouponEditRequest couponRequest = CouponEditRequest.builder() + .couponName("๋ณ€๊ฒฝ๋œ ์ฟ ํฐ์ด๋ฆ„") + .discountPrice(99_999L) + .minPrice(999_999L) + .limitCount(999) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + + // when + couponService.editCoupon(store.getId(), coupon.getId(), couponRequest); + + Coupon result = couponRepository.findById(savedCoupon.getId()).get(); + assertThat(result.getCouponName()).isEqualTo("๋ณ€๊ฒฝ๋œ ์ฟ ํฐ์ด๋ฆ„"); + assertThat(result.getDiscountPrice()).isEqualTo(99_999L); + assertThat(result.getEndDate()).isEqualTo(LocalDate.now()); + + } + + @DisplayName("๊ฐ€๊ฒŒId์ •๋ณด๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ์ฟ ํฐ์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์—†๋‹ค") + @Test + void cannotEditCouponWhenStoreIdMismatches() { + // given + Store store = createStore(); + storeRepository.save(store); + Coupon coupon = couponCreator(store); + couponRepository.save(coupon); + CouponEditRequest couponRequest = CouponEditRequest.builder() + .couponName("๋ณ€๊ฒฝ๋œ ์ฟ ํฐ์ด๋ฆ„") + .discountPrice(99_999L) + .minPrice(999_999L) + .limitCount(999) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + + Long wrongStoreId = 999L; + + // when // then + assertThatThrownBy(() -> couponService.editCoupon(wrongStoreId, coupon.getId(), couponRequest)) + .isInstanceOf(UnAuthorizedCouponException.class) + .hasMessage("ํ•ด๋‹น ์ฟ ํฐ์— ๋Œ€ํ•œ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."); + + } + + + @DisplayName("์ฟ ํฐ์„ ์‚ญ์ œํ•œ๋‹ค") + @Test + void softDeleteCoupon() { + // given + Store store = createStore(); + storeRepository.save(store); + Coupon coupon = couponCreator(store); + couponRepository.save(coupon); + + // when + couponService.softDeleteCoupon(store.getId(),coupon.getId()); + Coupon coupon1 = couponRepository.findById(coupon.getId()).get(); + + // then + assertThat(coupon1.getIsDeleted()).isTrue(); + + } + + @DisplayName("๊ฐ€๊ฒŒId์ •๋ณด๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์œผ๋ฉด ์ฟ ํฐ์„ ์‚ญ์ œํ•  ์ˆ˜ ์—†๋‹ค") + @Test + void cannotDeleteCouponWhenStoreIdMismatches() { + // given + Store store = createStore(); + storeRepository.save(store); + Coupon coupon = couponCreator(store); + couponRepository.save(coupon); + + Long wrongStoreId = 999L; + + // when // then + assertThatThrownBy(() -> couponService.softDeleteCoupon(wrongStoreId, coupon.getId())) + .isInstanceOf(UnAuthorizedCouponException.class) + .hasMessage("ํ•ด๋‹น ์ฟ ํฐ์— ๋Œ€ํ•œ ๊ถŒํ•œ์ด ์—†์Šต๋‹ˆ๋‹ค."); + + } + + @DisplayName("๋™์‹œ์— ๋“ค์–ด์˜จ ์ฟ ํฐ์„ ๋ชจ๋‘ ์‚ฌ์šฉํ•œ๋‹ค") + @Test + void useAllCoupons() { + // given + Store store = createStore(); + storeRepository.save(store); + + Coupon c1 = couponCreator(store); + Coupon c2 = couponCreator(store); + Coupon c3 = couponCreator(store); + couponRepository.saveAll(List.of(c1,c2,c3)); + + Long userId = 1L; + LocalDate useDate = LocalDate.now(); + + IssuedCoupon ic1 = createIssuedCoupon(c1,userId); + IssuedCoupon ic2 = createIssuedCoupon(c2,userId); + IssuedCoupon ic3 = createIssuedCoupon(c3,userId); + issuedCouponRepository.saveAll(List.of(ic1, ic2, ic3)); + + List couponIds = List.of(c1.getId(), c2.getId(), c3.getId()); + List issuedCouponIds = List.of(ic1.getId(), ic2.getId(), ic3.getId()); + + // when + couponService.useAllCoupons(couponIds, userId, useDate); + + List result = issuedCouponRepository.findAllById(issuedCouponIds); + + // then + assertThat(result).hasSize(3) + .extracting("isUsed") + .contains(true); + + } + + @DisplayName("์ฟ ํฐ ์‚ฌ์šฉ์„ ๋ฌดํšจํ™”ํ•œ๋‹ค") + @Test + void unUseAllCoupons() { + // given + Store store = createStore(); + storeRepository.save(store); + + Coupon c1 = couponCreator(store); + Coupon c2 = couponCreator(store); + Coupon c3 = couponCreator(store); + couponRepository.saveAll(List.of(c1,c2,c3)); + + Long userId = 1L; + + IssuedCoupon ic1 = createIssuedCoupon(c1,userId); + IssuedCoupon ic3 = createIssuedCoupon(c3,userId); + issuedCouponRepository.saveAll(List.of(ic1, ic3)); + + List couponIds = List.of(c1.getId(), c3.getId()); + List issuedCouponIds = List.of(ic1.getId(), ic3.getId()); + + // when + couponService.unUseAllCoupons(couponIds, userId); + + List result = issuedCouponRepository.findAllById(issuedCouponIds); + + // then + assertThat(result).hasSize(2) + .extracting("isUsed") + .contains(false); + + } + + @DisplayName("๋ฉ€ํ‹ฐ์“ฐ๋ ˆ๋“œ ํ™˜๊ฒฝ์—์„œ๋„ ๋™์‹œ์— ์ฟ ํฐ ๋ฐœ๊ธ‰์„ ์š”์ฒญํ•ด๋„ ์ •ํ•ด์ง„ ์ˆ˜๋Ÿ‰๋งŒํผ์˜ ๋ฐœ๊ธ‰์ด ๋ณด์žฅ๋œ๋‹ค") + @Test + void issueCouponInMultiThread() throws InterruptedException, ExecutionException { + // given + int limitCount = 100; + int applicantsCount = 200; + final String nickname = "nickname"; + final String phoneNumber = "phoneNumber"; + ExecutorService executorService = Executors.newFixedThreadPool(32); + CountDownLatch latch = new CountDownLatch(applicantsCount); + + Future couponCreate = executorService.submit(() -> { + Store store = createStore(); + storeRepository.save(store); + + Coupon coupon = couponCreator(store, limitCount); + couponRepository.save(coupon).getId(); + + String redisKey = RedisUtils.makeRedisKey(coupon); + redisOperation.addAndSetExpr(redisKey, LocalDate.now().plusDays(1)); + + return coupon.getId(); + + }); + + final Long couponId = couponCreate.get(); + + // when + LongStream.rangeClosed(1L, applicantsCount) + .forEach(userId -> + executorService.submit(() -> { + try { + couponService.downloadCoupon(userId,couponId,nickname,phoneNumber,LocalDate.now()); + } catch (Exception ignored) { + } finally { + latch.countDown(); + } + }) + ); + + latch.await(); + + long issuedCouponCount = issuedCouponRepository.count(); + + // then + assertThat(issuedCouponCount).isEqualTo(limitCount); + + } + + @DisplayName("์š”์ฒญ๋ฐ›์€ ์ฟ ํฐ๊ฐ€๊ฒฉ์ด ์›๋ณธ ์ฟ ํฐ๊ฐ€๊ฒฉ๊ณผ ๋‹ค๋ฅด๋ฉด ์ฟ ํฐ์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋‹ค") + @Test + void validateCouponPrice() { + // given + Store store = createStore(); + storeRepository.save(store); + Long discountPrice = 10_000L; + Long minPrice = 100_000L; + + Coupon coupon = createCouponWithPrice(store, discountPrice, minPrice); + couponRepository.save(coupon); + + ValidatePriceDto validatePriceDto = ValidatePriceDto.builder() + .couponId(coupon.getId()) + .storeId(store.getId()) + .actualAmount(100_000L) + .couponAmount(100_000L) + .build(); + List data = List.of(validatePriceDto); + + // when // then + assertThatThrownBy(() -> couponService.validateCouponPrice(data)) + .isInstanceOf(CouponInconsistencyException.class) + .hasMessage("ํ•ด๋‹น ์š”์ฒญ์€ ์‹ค์ œ ์ฟ ํฐ ์ •๋ณด์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + + } + + @DisplayName("์ตœ์†Œ์ด์šฉ๊ธˆ์•ก์„ ๋งŒ์กฑํ•˜์ง€ ๋ชปํ•œ ์ฟ ํฐ์˜ ์‚ฌ์šฉ ์š”์ฒญ์€ ๊ฑฐ๋ถ€๋œ๋‹ค") + @Test + void validateCouponPrice2() { + // given + Store store = createStore(); + storeRepository.save(store); + Long discountPrice = 10_000L; + Long minPrice = 100_000L; + + Coupon coupon = createCouponWithPrice(store, discountPrice, minPrice); + couponRepository.save(coupon); + + ValidatePriceDto validatePriceDto = ValidatePriceDto.builder() + .couponId(coupon.getId()) + .storeId(store.getId()) + .actualAmount(10_000L) + .couponAmount(10_000L) + .build(); + + // when // then + assertThatThrownBy(() -> couponService.validateCouponPrice(List.of(validatePriceDto))) + .isInstanceOf(CouponInconsistencyException.class) + .hasMessage("ํ•ด๋‹น ์š”์ฒญ์€ ์‹ค์ œ ์ฟ ํฐ ์ •๋ณด์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + + } + + private IssuedCoupon createIssuedCoupon(Coupon coupon, Long userId) { + return IssuedCoupon.builder() + .id(createIssuedCouponId(coupon.getId(),userId)) + .coupon(coupon) + .build(); + } + + private IssuedCouponId createIssuedCouponId(Long couponId, Long userId) { + return IssuedCouponId.builder() + .couponId(couponId) + .userId(userId) + .build(); + } + + private Store createStore() { + return Store.builder() + .storeManagerId(1L) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private Coupon couponCreator(Store store) { + return Coupon.builder() + .couponCode(UUID.randomUUID().toString()) + .store(store) + .limitCount(100) + .couponName("์ฟ ํฐ์ด๋ฆ„") + .discountPrice(10000L) + .minPrice(100000L) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + } + + private Coupon couponCreator(Store store, int limitCount) { + return Coupon.builder() + .couponCode(UUID.randomUUID().toString()) + .store(store) + .limitCount(limitCount) + .couponName("์ฟ ํฐ์ด๋ฆ„") + .discountPrice(10000L) + .minPrice(100000L) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + } + + private Coupon createCouponWithPrice(Store store, Long discountPrice, Long minPrice) { + return Coupon.builder() + .couponCode(UUID.randomUUID().toString()) + .store(store) + .limitCount(100) + .couponName("์ฟ ํฐ์ด๋ฆ„") + .discountPrice(discountPrice) + .minPrice(minPrice) + .startDate(LocalDate.now()) + .endDate(LocalDate.now()) + .build(); + + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/question/handler/AnswerCreatorTest.java b/src/test/java/kr/bb/store/domain/question/handler/AnswerCreatorTest.java new file mode 100644 index 0000000..2ad9ccd --- /dev/null +++ b/src/test/java/kr/bb/store/domain/question/handler/AnswerCreatorTest.java @@ -0,0 +1,102 @@ +package kr.bb.store.domain.question.handler; + +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.question.entity.Answer; +import kr.bb.store.domain.question.entity.Question; +import kr.bb.store.domain.question.repository.AnswerRepository; +import kr.bb.store.domain.question.repository.QuestionRepository; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class AnswerCreatorTest extends BasicIntegrationTestEnv { + @Autowired + private AnswerCreator answerCreator; + @Autowired + private QuestionRepository questionRepository; + @Autowired + private AnswerRepository answerRepository; + @Autowired + private StoreRepository storeRepository; + + @DisplayName("๋‹ต๋ณ€ ์ •๋ณด๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•œ๋‹ค") + @Test + void createAnswer() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question question = createQuestion(store); + questionRepository.save(question); + + String content = "๋‹ต๋ณ€๊ธ€"; + + // when + Answer answer = answerCreator.create(question,content); + answerRepository.save(answer); + + // then + assertThat(answer.getQuestion()).isNotNull(); + assertThat(answer.getContent()).isEqualTo(content); + + } + + @DisplayName("๋‹ต๋ณ€์€ ํ•ด๋‹น ์งˆ๋ฌธ๊ณผ ๋™์ผํ•œ Id๊ฐ’์„ ๊ฐ€์ ธ์•ผ ํ•œ๋‹ค") + @Test + void AnswerMustHaveSameIdWithQuestion() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question q1 = createQuestion(store); + Question q2 = createQuestion(store); + Question q3 = createQuestion(store); + Question q4 = createQuestion(store); + Question q5 = createQuestion(store); + questionRepository.saveAll(List.of(q1,q2,q3,q4,q5)); + + String content = "๋‹ต๋ณ€๊ธ€"; + + // when + Answer answer = answerCreator.create(q5,content); + answerRepository.save(answer); + + // then + assertThat(answer.getId()).isEqualTo(q5.getId()); + + } + + + + private Question createQuestion(Store store) { + return Question.builder() + .store(store) + .userId(1L) + .nickname("๋‹‰๋„ค์ž„") + .productId("1") + .productName("์ƒํ’ˆ๋ช…") + .title("์งˆ๋ฌธ์ œ๋ชฉ") + .content("์งˆ๋ฌธ๋‚ด์šฉ") + .isSecret(true) + .build(); + } + + private Store createStore(Long userId) { + return Store.builder() + .storeManagerId(userId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/question/handler/QuestionCreatorTest.java b/src/test/java/kr/bb/store/domain/question/handler/QuestionCreatorTest.java new file mode 100644 index 0000000..d378f52 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/question/handler/QuestionCreatorTest.java @@ -0,0 +1,99 @@ +package kr.bb.store.domain.question.handler; + +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.question.controller.request.QuestionCreateRequest; +import kr.bb.store.domain.question.entity.Question; +import kr.bb.store.domain.question.repository.QuestionRepository; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.persistence.EntityManager; + +import static org.assertj.core.api.Assertions.assertThat; + +class QuestionCreatorTest extends BasicIntegrationTestEnv { + @Autowired + private QuestionCreator questionCreator; + @Autowired + private QuestionRepository questionRepository; + @Autowired + private StoreRepository storeRepository; + @Autowired + private EntityManager em; + + + @DisplayName("์งˆ๋ฌธ ์ •๋ณด๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ์งˆ๋ฌธ์„ ์ƒ์„ฑํ•œ๋‹ค") + @Test + void createQuestion() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Long customerId = 2L; + + QuestionCreateRequest questionCreateRequest = createQuestionCreateRequest(store.getId()); + + // when + Question question = questionCreator.create(customerId, questionCreateRequest); + + // then + assertThat(question.getId()).isNotNull(); + assertThat(question.getStore()).isNotNull(); + assertThat(question.getTitle()).isEqualTo("์งˆ๋ฌธ์ œ๋ชฉ"); + + } + + @DisplayName("์งˆ๋ฌธ์ด ์ฒ˜์Œ ์ƒ์„ฑ๋์„ ๋•Œ ํ™•์ธ ์—ฌ๋ถ€๋Š” ํ•ญ์ƒ '์ฝ์ง€ ์•Š์Œ'์ด๋‹ค") + @Test + void isReadAlwaysFalseWhenQuestionCreated() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Long customerId = 2L; + + QuestionCreateRequest questionCreateRequest = createQuestionCreateRequest(store.getId()); + + // when + Question question = questionCreator.create(customerId, questionCreateRequest); + + em.flush(); + em.clear(); + + Question savedQuestion = questionRepository.findById(question.getId()).get(); + + // then + assertThat(savedQuestion.getIsRead()).isFalse(); + } + + + + private QuestionCreateRequest createQuestionCreateRequest(Long storeId) { + return QuestionCreateRequest.builder() + .storeId(storeId) + .productId("1") + .productName("์ œํ’ˆ๋ช…") + .title("์งˆ๋ฌธ์ œ๋ชฉ") + .content("์งˆ๋ฌธ๋‚ด์šฉ") + .isSecret(true) + .nickname("๋‹‰๋„ค์ž„") + .build(); + } + + private Store createStore(Long userId) { + return Store.builder() + .storeManagerId(userId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/question/handler/QuestionReaderTest.java b/src/test/java/kr/bb/store/domain/question/handler/QuestionReaderTest.java new file mode 100644 index 0000000..1c5e87d --- /dev/null +++ b/src/test/java/kr/bb/store/domain/question/handler/QuestionReaderTest.java @@ -0,0 +1,403 @@ +package kr.bb.store.domain.question.handler; + +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.question.controller.response.QuestionDetailInfoResponse; +import kr.bb.store.domain.question.dto.MyQuestionInMypageDto; +import kr.bb.store.domain.question.dto.QuestionForOwnerDto; +import kr.bb.store.domain.question.dto.QuestionInProductDto; +import kr.bb.store.domain.question.entity.Answer; +import kr.bb.store.domain.question.entity.Question; +import kr.bb.store.domain.question.repository.AnswerRepository; +import kr.bb.store.domain.question.repository.QuestionRepository; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; + +import javax.persistence.EntityManager; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +class QuestionReaderTest extends BasicIntegrationTestEnv { + @Autowired + private QuestionReader questionReader; + @Autowired + private QuestionRepository questionRepository; + @Autowired + private AnswerRepository answerRepository; + @Autowired + private StoreRepository storeRepository; + @Autowired + private EntityManager em; + + + @DisplayName("์งˆ๋ฌธ Id๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์งˆ๋ฌธ ์ƒ์„ธ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜จ๋‹ค") + @Test + void readDetailInfo() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question question = createQuestion(store); + questionRepository.save(question); + + Answer answer = createAnswer(question); + answerRepository.save(answer); + + String nickname = "์œ ์ €๋ช…"; + String productName = "๊ฐ€๊ฒŒ๋ช…"; + + // when + QuestionDetailInfoResponse questionDetailInfoResponse = questionReader.readDetailInfo(question.getId()); + + // then + assertThat(questionDetailInfoResponse.getTitle()).isEqualTo("์งˆ๋ฌธ์ œ๋ชฉ"); + assertThat(questionDetailInfoResponse.getAnswer().getContent()).isEqualTo("๋‹ต๋ณ€๋‚ด์šฉ"); + + } + + @DisplayName("์งˆ๋ฌธ ์ƒ์„ธ์ •๋ณด ์š”์ฒญ ์‹œ ๋‹ต๋ณ€์ด ์—†๋‹ค๋ฉด ๋‹ต๋ณ€๋งŒ null๋กœ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void AnswerInQuestionDetailCanBeNull() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question question = createQuestion(store); + questionRepository.save(question); + + String nickname = "์œ ์ €๋ช…"; + String productName = "๊ฐ€๊ฒŒ๋ช…"; + + // when + QuestionDetailInfoResponse questionDetailInfoResponse = questionReader.readDetailInfo(question.getId()); + + // then + assertThat(questionDetailInfoResponse.getAnswer()).isNull(); + assertThat(questionDetailInfoResponse.getTitle()).isEqualTo("์งˆ๋ฌธ์ œ๋ชฉ"); + } + + @DisplayName("์งˆ๋ฌธ ์ƒ์„ธ์ •๋ณด ์š”์ฒญ ์‹œ ํ•ด๋‹น ์งˆ๋ฌธ์€ ์ฝ์Œ์ฒ˜๋ฆฌ๋œ๋‹ค") + @Test + void isReadWillTrueWhenReadDetailInfo() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question question = createQuestion(store); + questionRepository.save(question); + + String nickname = "์œ ์ €๋ช…"; + String productName = "๊ฐ€๊ฒŒ๋ช…"; + + // when + questionReader.readDetailInfo(question.getId()); + + em.flush(); + em.clear(); + + Question savedQuestion = questionRepository.findById(question.getId()).get(); + + // then + assertThat(savedQuestion.getIsRead()).isTrue(); + + } + + @DisplayName("isReplied๊ฐ€ null์ผ ๋•Œ ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ๋ชจ๋“  ์งˆ๋ฌธ์„ ๊ฐ€์ ธ์˜จ๋‹ค") + @Test + void readQuestionsForStoreOwner() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question q1 = createQuestion(store); + Question q2 = createQuestion(store); + Question q3 = createQuestion(store); + Question q4 = createQuestion(store); + Question q5 = createQuestion(store); + questionRepository.saveAll(List.of(q1,q2,q3,q4,q5)); + + Answer a1 = createAnswer(q1); + Answer a2 = createAnswer(q2); + Answer a3 = createAnswer(q3); + answerRepository.saveAll(List.of(a1,a2,a3)); + + PageRequest pageRequest = PageRequest.of(0, 10); + + Boolean isReplied = null; + + // when + Page result = + questionReader.readQuestionsForStoreOwner(store.getId(), isReplied, pageRequest); + + // then + assertThat(result.getTotalElements()).isEqualTo(5); + assertThat(result.getContent()).extracting("isReplied") + .containsExactlyInAnyOrder( + true,true,true,false,false + ); + + } + + @DisplayName("isReplied๊ฐ€ true์ผ ๋•Œ ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ์งˆ๋ฌธ ์ค‘ ๋‹ต๋ณ€ํ•œ ์งˆ๋ฌธ๋งŒ ๊ฐ€์ ธ์˜จ๋‹ค") + @Test + void readRepliedQuestionsForStoreOwner() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question q1 = createQuestion(store); + Question q2 = createQuestion(store); + Question q3 = createQuestion(store); + Question q4 = createQuestion(store); + Question q5 = createQuestion(store); + questionRepository.saveAll(List.of(q1,q2,q3,q4,q5)); + + Answer a1 = createAnswer(q1); + Answer a2 = createAnswer(q2); + Answer a3 = createAnswer(q3); + answerRepository.saveAll(List.of(a1,a2,a3)); + + PageRequest pageRequest = PageRequest.of(0, 10); + + Boolean isReplied = true; + + // when + Page result = + questionReader.readQuestionsForStoreOwner(store.getId(), isReplied, pageRequest); + + // then + assertThat(result.getTotalElements()).isEqualTo(3); + + } + + @DisplayName("isReplied๊ฐ€ false์ผ ๋•Œ ํ•ด๋‹น ๊ฐ€๊ฒŒ์˜ ์งˆ๋ฌธ ์ค‘ ๋‹ต๋ณ€ํ•˜์ง€ ์•Š์€ ์งˆ๋ฌธ๋งŒ ๊ฐ€์ ธ์˜จ๋‹ค") + @Test + void readNonRepliedQuestionsForStoreOwner() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question q1 = createQuestion(store); + Question q2 = createQuestion(store); + Question q3 = createQuestion(store); + Question q4 = createQuestion(store); + Question q5 = createQuestion(store); + questionRepository.saveAll(List.of(q1,q2,q3,q4,q5)); + + Answer a1 = createAnswer(q1); + Answer a2 = createAnswer(q2); + Answer a3 = createAnswer(q3); + answerRepository.saveAll(List.of(a1,a2,a3)); + + PageRequest pageRequest = PageRequest.of(0, 10); + + Boolean isReplied = false; + + // when + Page result = + questionReader.readQuestionsForStoreOwner(store.getId(), isReplied, pageRequest); + + // then + assertThat(result.getTotalElements()).isEqualTo(2); + + } + + @DisplayName("ํ•ด๋‹น ์ƒํ’ˆ์— ๋‹ฌ๋ฆฐ ๋ฌธ์˜๋ฅผ ์กฐํšŒํ•œ๋‹ค") + @Test + void readQuestionsInProduct() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question q1 = createQuestionWithProductIdAndUserId(store,"1",2L); + Question q2 = createQuestionWithProductIdAndUserId(store,"1",2L); + Question q3 = createQuestionWithProductIdAndUserId(store,"1",3L); + Question q4 = createQuestionWithProductIdAndUserId(store,"2",3L); + Question q5 = createQuestionWithProductIdAndUserId(store,"2",2L); + questionRepository.saveAll(List.of(q1,q2,q3,q4,q5)); + + String productId = "1"; + Long userId = 2L; + Boolean isReplied = false; + PageRequest pageRequest = PageRequest.of(0, 10); + + // when + Page result = questionReader.readQuestionsInProduct(userId, productId, isReplied, pageRequest); + + // then + assertThat(result.getTotalElements()).isEqualTo(3); + } + + @DisplayName("๋‚ด๊ฐ€ ์ž‘์„ฑํ•˜์ง€ ์•Š์€ ๋น„๋ฐ€๊ธ€์€ ์ œ๋ชฉ๊ณผ ๋‚ด์šฉ์ด ๋น„๋ฐ€์ฒ˜๋ฆฌ๋œ๋‹ค") + @Test + void cannotSeeOthersSecretQuesetion() { + // given + Store store = createStore(1L); + storeRepository.save(store); + long writerId = 1L; + + Question q1 = createQuestion(store,writerId,true); + questionRepository.save(q1); + + long readerId = 2L; + String productId = "1"; + Boolean isReplied = false; + PageRequest pageRequest = PageRequest.of(0, 10); + + // when + Page questionInProductDtos = questionReader.readQuestionsInProduct(readerId, productId, isReplied, pageRequest); + QuestionInProductDto result = questionInProductDtos.getContent().get(0); + + // then + assertThat(result.getTitle()).isEqualTo("๋น„๋ฐ€๊ธ€์ž…๋‹ˆ๋‹ค"); + assertThat(result.getContent()).isEqualTo(""); + + } + + @DisplayName("๋น„ํšŒ์›์€ ๋น„๋ฐ€๊ธ€์„ ๋ณผ ์ˆ˜ ์—†๋‹ค") + @Test + void nonMembersCannotSeeSecretQuestion() { + // given + Store store = createStore(1L); + storeRepository.save(store); + long writerId = 1L; + + Question q1 = createQuestion(store,writerId,true); + questionRepository.save(q1); + + Long nonMemberId = null; + String productId = "1"; + Boolean isReplied = false; + PageRequest pageRequest = PageRequest.of(0, 10); + + // when + Page questionInProductDtos = questionReader.readQuestionsInProduct(nonMemberId, productId, isReplied, pageRequest); + QuestionInProductDto result = questionInProductDtos.getContent().get(0); + + // then + assertThat(result.getTitle()).isEqualTo("๋น„๋ฐ€๊ธ€์ž…๋‹ˆ๋‹ค"); + assertThat(result.getContent()).isEqualTo(""); + } + + + + + @DisplayName("ํ•ด๋‹น ์ƒํ’ˆ์—์„œ ๋‚ด๊ฐ€ ๋‚จ๊ธด ๋ฌธ์˜๋งŒ ๋ชจ์•„๋ณธ๋‹ค") + @Test + void readMyQuestionsInProduct() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question q1 = createQuestionWithProductIdAndUserId(store,"1",2L); + Question q2 = createQuestionWithProductIdAndUserId(store,"1",2L); + Question q3 = createQuestionWithProductIdAndUserId(store,"1",3L); + Question q4 = createQuestionWithProductIdAndUserId(store,"2",3L); + Question q5 = createQuestionWithProductIdAndUserId(store,"2",2L); + questionRepository.saveAll(List.of(q1,q2,q3,q4,q5)); + + String productId = "1"; + Long userId = 2L; + Boolean isReplied = false; + PageRequest pageRequest = PageRequest.of(0, 10); + + // when + Page result = questionReader.readMyQuestionsInProduct(userId, productId, isReplied, pageRequest); + + // then + assertThat(result.getTotalElements()).isEqualTo(2); + + } + + @DisplayName("๋‚ด๊ฐ€ ๋‚จ๊ธด ๋ชจ๋“  ๋ฌธ์˜๋ฅผ ํ™•์ธํ•œ๋‹ค") + @Test + void readMyQuestions() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question q1 = createQuestionWithProductIdAndUserId(store,"1",2L); + Question q2 = createQuestionWithProductIdAndUserId(store,"1",2L); + Question q3 = createQuestionWithProductIdAndUserId(store,"1",3L); + Question q4 = createQuestionWithProductIdAndUserId(store,"2",3L); + Question q5 = createQuestionWithProductIdAndUserId(store,"2",2L); + questionRepository.saveAll(List.of(q1,q2,q3,q4,q5)); + + Long userId = 2L; + Boolean isReplied = false; + PageRequest pageRequest = PageRequest.of(0, 10); + + // when + Page result = questionReader.readQuestionsForMypage(userId, isReplied, pageRequest); + + // then + assertThat(result.getTotalElements()).isEqualTo(3); + + } + + + private Question createQuestionWithProductIdAndUserId(Store store,String productId, Long userId) { + return Question.builder() + .store(store) + .userId(userId) + .nickname("๋‹‰๋„ค์ž„") + .productId(productId) + .productName("์ƒํ’ˆ๋ช…") + .title("์งˆ๋ฌธ์ œ๋ชฉ") + .content("์งˆ๋ฌธ๋‚ด์šฉ") + .isSecret(true) + .build(); + } + + private Question createQuestion(Store store) { + return Question.builder() + .store(store) + .userId(1L) + .nickname("๋‹‰๋„ค์ž„") + .productId("1") + .productName("์ƒํ’ˆ๋ช…") + .title("์งˆ๋ฌธ์ œ๋ชฉ") + .content("์งˆ๋ฌธ๋‚ด์šฉ") + .isSecret(true) + .build(); + } + private Question createQuestion(Store store, Long writerId, boolean isSecret) { + return Question.builder() + .store(store) + .userId(writerId) + .nickname("๋‹‰๋„ค์ž„") + .productId("1") + .productName("์ƒํ’ˆ๋ช…") + .title("์งˆ๋ฌธ์ œ๋ชฉ") + .content("์งˆ๋ฌธ๋‚ด์šฉ") + .isSecret(isSecret) + .build(); + } + + private Answer createAnswer(Question question) { + return Answer.builder() + .question(question) + .content("๋‹ต๋ณ€๋‚ด์šฉ") + .build(); + } + + private Store createStore(Long userId) { + return Store.builder() + .storeManagerId(userId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/question/service/QuestionServiceTest.java b/src/test/java/kr/bb/store/domain/question/service/QuestionServiceTest.java new file mode 100644 index 0000000..1e73c60 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/question/service/QuestionServiceTest.java @@ -0,0 +1,142 @@ +package kr.bb.store.domain.question.service; + +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.question.controller.request.QuestionCreateRequest; +import kr.bb.store.domain.question.controller.response.QuestionDetailInfoResponse; +import kr.bb.store.domain.question.entity.Answer; +import kr.bb.store.domain.question.entity.Question; +import kr.bb.store.domain.question.repository.AnswerRepository; +import kr.bb.store.domain.question.repository.QuestionRepository; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import static org.assertj.core.api.Assertions.assertThat; + + +class QuestionServiceTest extends BasicIntegrationTestEnv { + @Autowired + private QuestionService questionService; + @Autowired + private QuestionRepository questionRepository; + @Autowired + private AnswerRepository answerRepository; + @Autowired + private StoreRepository storeRepository; + + + @DisplayName("์งˆ๋ฌธ Id๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ ์งˆ๋ฌธ ์ƒ์„ธ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜จ๋‹ค") + @Test + void readDetailInfo() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question question = createQuestion(store); + questionRepository.save(question); + + Answer answer = createAnswer(question); + answerRepository.save(answer); + + // when + QuestionDetailInfoResponse questionDetailInfoResponse = questionService.getQuestionInfo(question.getId()); + + // then + assertThat(questionDetailInfoResponse.getTitle()).isEqualTo("์งˆ๋ฌธ์ œ๋ชฉ"); + assertThat(questionDetailInfoResponse.getAnswer().getContent()).isEqualTo("๋‹ต๋ณ€๋‚ด์šฉ"); + + } + + @DisplayName("์งˆ๋ฌธ ์ •๋ณด๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ์งˆ๋ฌธ์„ ์ƒ์„ฑํ•œ๋‹ค") + @Test + void createQuestion() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Long customerId = 2L; + + QuestionCreateRequest questionCreateRequest = createQuestionCreateRequest(store.getId()); + + // when + Question question = questionService.createQuestion(customerId, questionCreateRequest); + + // then + assertThat(question.getId()).isNotNull(); + assertThat(question.getStore()).isNotNull(); + assertThat(question.getTitle()).isEqualTo("์งˆ๋ฌธ์ œ๋ชฉ"); + + } + + @DisplayName("๋‹ต๋ณ€ ์ •๋ณด๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ๋‹ต๋ณ€์„ ์ƒ์„ฑํ•œ๋‹ค") + @Test + void createAnswer() { + // given + Store store = createStore(1L); + storeRepository.save(store); + + Question question = createQuestion(store); + questionRepository.save(question); + + String content = "๋‹ต๋ณ€๊ธ€"; + + // when + Answer answer = questionService.createAnswer(question,content); + answerRepository.save(answer); + + // then + assertThat(answer.getQuestion()).isNotNull(); + assertThat(answer.getContent()).isEqualTo(content); + + } + + + + private QuestionCreateRequest createQuestionCreateRequest(Long storeId) { + return QuestionCreateRequest.builder() + .storeId(storeId) + .productId("1") + .productName("์ƒํ’ˆ๋ช…") + .title("์งˆ๋ฌธ์ œ๋ชฉ") + .content("์งˆ๋ฌธ๋‚ด์šฉ") + .isSecret(true) + .nickname("๋‹‰๋„ค์ž„") + .build(); + } + + private Question createQuestion(Store store) { + return Question.builder() + .store(store) + .userId(1L) + .productName("์ƒํ’ˆ๋ช…") + .productId("1") + .title("์งˆ๋ฌธ์ œ๋ชฉ") + .content("์งˆ๋ฌธ๋‚ด์šฉ") + .isSecret(true) + .nickname("๋‹‰๋„ค์ž„") + .build(); + } + + private Answer createAnswer(Question question) { + return Answer.builder() + .question(question) + .content("๋‹ต๋ณ€๋‚ด์šฉ") + .build(); + } + + private Store createStore(Long userId) { + return Store.builder() + .storeManagerId(userId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/store/controller/StoreControllerTest.java b/src/test/java/kr/bb/store/domain/store/controller/StoreControllerTest.java new file mode 100644 index 0000000..517bc9e --- /dev/null +++ b/src/test/java/kr/bb/store/domain/store/controller/StoreControllerTest.java @@ -0,0 +1,98 @@ +package kr.bb.store.domain.store.controller; + +import com.fasterxml.jackson.databind.ObjectMapper; +import kr.bb.store.client.ProductFeignClient; +import kr.bb.store.domain.store.controller.request.StoreCreateRequest; +import kr.bb.store.domain.store.facade.StoreFacade; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.redisson.api.RedissonClient; +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.web.servlet.MockMvc; +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; +import org.springframework.test.web.servlet.result.MockMvcResultHandlers; +import org.springframework.test.web.servlet.result.MockMvcResultMatchers; + +@WebMvcTest(controllers = StoreController.class) +class StoreControllerTest { + @Autowired + private MockMvc mockMvc; + @Autowired + private ObjectMapper objectMapper; + @MockBean + private StoreFacade storeFacade; + + @DisplayName("๊ฐ€๊ฒŒ์ƒ์„ฑ ์‹œ ์š”์ฒญ๊ฐ’์€ ๋ชจ๋‘ null์ด ์•„๋‹ˆ์—ฌ์•ผ ํ•œ๋‹ค") + @Test + void storeCreateRequestPropertiesCannotBeNull() throws Exception { + // given + StoreCreateRequest storeCreateRequest = createStoreCreateRequest(); + + // when // then + mockMvc.perform(MockMvcRequestBuilders.post("/") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(storeCreateRequest)) + .header("userId",1L) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isOk()); + } + + @DisplayName("๊ฐ€๊ฒŒ์ƒ์„ฑ ์‹œ ์š”์ฒญ๊ฐ’์€ ๋ชจ๋‘ null์ด ์•„๋‹ˆ์—ฌ์•ผ ํ•œ๋‹ค") + @Test + void storeCreateRequestPropertiesCannotBeNull2() throws Exception { + // given + StoreCreateRequest storeCreateRequest = StoreCreateRequest.builder() + .storeName(null) + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .deliveryPrice(5_000L) + .freeDeliveryMinPrice(10_000L) + .sido("์„œ์šธ") + .gugun("๊ฐ•๋‚จ๊ตฌ") + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(33.33322D) + .lon(127.13123D) + .build(); + + // when // then + mockMvc.perform(MockMvcRequestBuilders.post("/") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(storeCreateRequest)) + ) + .andDo(MockMvcResultHandlers.print()) + .andExpect(MockMvcResultMatchers.status().isBadRequest()); + } + + + + + private StoreCreateRequest createStoreCreateRequest() { + return StoreCreateRequest.builder() + .storeName("๊ฐ€๊ฒŒ1") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .deliveryPrice(5_000L) + .freeDeliveryMinPrice(10_000L) + .sido("์„œ์šธ") + .gugun("๊ฐ•๋‚จ๊ตฌ") + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(33.33322D) + .lon(127.13123D) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/store/facade/StoreFacadeTest.java b/src/test/java/kr/bb/store/domain/store/facade/StoreFacadeTest.java new file mode 100644 index 0000000..632f56e --- /dev/null +++ b/src/test/java/kr/bb/store/domain/store/facade/StoreFacadeTest.java @@ -0,0 +1,243 @@ +package kr.bb.store.domain.store.facade; + +import bloomingblooms.domain.flower.FlowerDto; +import bloomingblooms.domain.product.StoreSubscriptionProductId; +import bloomingblooms.response.CommonResponse; +import kr.bb.store.client.ProductFeignClient; +import kr.bb.store.client.StoreLikeFeignClient; +import kr.bb.store.client.StoreSubscriptionFeignClient; +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.cargo.entity.FlowerCargo; +import kr.bb.store.domain.cargo.repository.FlowerCargoRepository; +import kr.bb.store.domain.store.controller.request.StoreCreateRequest; +import kr.bb.store.domain.store.controller.response.StoreForMapResponse; +import kr.bb.store.domain.store.controller.response.StoreInfoUserResponse; +import kr.bb.store.domain.store.controller.response.StoreListResponse; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.GugunRepository; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.entity.address.SidoRepository; +import kr.bb.store.domain.store.repository.StoreAddressRepository; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.BDDMockito; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageRequest; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.EntityManager; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; + +@SpringBootTest +@Transactional +class StoreFacadeTest extends BasicIntegrationTestEnv { + @Autowired + private StoreFacade storeFacade; + @Autowired + private StoreRepository storeRepository; + @Autowired + private SidoRepository sidoRepository; + @Autowired + private GugunRepository gugunRepository; + @Autowired + private FlowerCargoRepository flowerCargoRepository; + @Autowired + private StoreAddressRepository storeAddressRepository; + @Autowired + private EntityManager em; + @MockBean + private ProductFeignClient productFeignClient; + @MockBean + private StoreLikeFeignClient storeLikeFeignClient; + @MockBean + private StoreSubscriptionFeignClient storeSubscriptionFeignClient; + + @DisplayName("store์ƒ์„ฑ ์‹œ ์žฌ๊ณ ์ •๋ณด๋„ ํ•จ๊ป˜ ์ƒ์„ฑ๋œ๋‹ค") + @Test + void createStore() { + // given + Long userId = 1L; + StoreCreateRequest storeCreateRequest = createStoreCreateRequest(); + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + FlowerDto flowerDto = FlowerDto.builder() + .flowerId(1L) + .flowerName("์žฅ๋ฏธ๊ฝƒ") + .build(); + CommonResponse> data = CommonResponse.>builder().data(List.of(flowerDto)).build(); + BDDMockito.given(productFeignClient.getFlowers()) + .willReturn(data); + + // when + Long storeId = storeFacade.createStore(userId, storeCreateRequest); + + em.flush(); + em.clear(); + + FlowerCargo result = flowerCargoRepository.findAllByStoreId(storeId).get(0); + + // then + assertThat(result.getFlowerName()).isEqualTo("์žฅ๋ฏธ๊ฝƒ"); + assertThat(result.getStock()).isEqualTo(0); + } + + @DisplayName("๊ฐ€๊ฒŒ์ •๋ณด๋ฅผ ์ข‹์•„์š” ์—ฌ๋ถ€์™€ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void getStoresWithLikes() { + // given + Store store1 = createStoreEntity(100L, "๊ฐ€๊ฒŒ1"); + Store store2 = createStoreEntity(100L, "๊ฐ€๊ฒŒ2"); + Store store3 = createStoreEntity(100L, "๊ฐ€๊ฒŒ3"); + storeRepository.saveAll(List.of(store1,store2,store3)); + Long userId = 1L; + PageRequest page = PageRequest.of(0, 5); + Map data = Map.of(store1.getId(),true,store2.getId(),true, store3.getId(), false); + BDDMockito.given(storeLikeFeignClient.getStoreLikes(any(), any())) + .willReturn(CommonResponse.>builder().data(data).build()); + // when + List result = storeFacade.getStoresWithLikes(userId, page).getStores(); + + // then + assertThat(result).hasSize(3) + .extracting("isLiked") + .containsExactly(true, true, false); + + } + + @DisplayName("์ข‹์•„์š”์™€ ๊ตฌ๋…์—ฌ๋ถ€๋ฅผ ํฌํ•จํ•œ ๊ฐ€๊ฒŒ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void getStoreInfoForUser() { + // given + Store store = createStoreEntity(100L, "๊ฐ€๊ฒŒ1"); + storeRepository.save(store); + + StoreAddress storeAddress = createStoreAddressEntity(store,0D,0D); + storeAddressRepository.save(storeAddress); + + Long userId = 1L; + StoreSubscriptionProductId subscriptionId = StoreSubscriptionProductId.builder() + .subscriptionProductId("๊ตฌ๋…์ƒํ’ˆ ์•„์ด๋””") + .build(); + BDDMockito.given(productFeignClient.getSubscriptionProductId(any())) + .willReturn(CommonResponse.builder().data(subscriptionId).build()); + Map likeData = Map.of(store.getId(), true); + BDDMockito.given(storeLikeFeignClient.getStoreLikes(any(), any())) + .willReturn(CommonResponse.>builder().data(likeData).build()); + Map subData = Map.of(store.getId(), false); + BDDMockito.given(storeSubscriptionFeignClient.getStoreSubscriptions(any(), any())) + .willReturn(CommonResponse.>builder().data(subData).build()); + + // when + StoreInfoUserResponse storeInfoForUser = storeFacade.getStoreInfoForUser(userId, store.getId()); + + // then + assertThat(storeInfoForUser.getSubscriptionProductId()).isEqualTo("๊ตฌ๋…์ƒํ’ˆ ์•„์ด๋””"); + assertThat(storeInfoForUser.getIsLiked()).isTrue(); + assertThat(storeInfoForUser.getIsSubscribed()).isFalse(); + } + + @DisplayName("์œ„์น˜๊ธฐ๋ฐ˜ ๊ฐ€๊ฒŒ๋ฅผ ๋ฐ˜ํ™˜ํ•  ๋•Œ ์ข‹์•„์š” ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void getStoresWithRegion() { + // given + Sido sido1 = new Sido("1", "์„œ์šธ"); + Gugun gugun1 = new Gugun("100",sido1,"๊ฐ•๋‚จ๊ตฌ"); + Long userId = 1L; + Store s1 = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + storeRepository.save(s1); + StoreAddress sa1 = createStoresAddressWithSidoGugun(s1, sido1, gugun1); + storeAddressRepository.save(sa1); + + Map data = Map.of(s1.getId(), true); + BDDMockito.given(storeLikeFeignClient.getStoreLikes(any(), any())) + .willReturn(CommonResponse.>builder().data(data).build()); + + // when + List stores = storeFacade.getStoresWithRegion(userId, sido1.getCode(), gugun1.getCode()).getStores(); + + // then + assertThat(stores) + .hasSize(1) + .extracting("isLiked") + .containsExactly(true); + } + + + private StoreCreateRequest createStoreCreateRequest() { + return StoreCreateRequest.builder() + .storeName("๊ฐ€๊ฒŒ1") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .deliveryPrice(5_000L) + .freeDeliveryMinPrice(10_000L) + .sido("์„œ์šธ") + .gugun("๊ฐ•๋‚จ๊ตฌ") + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(33.333220D) + .lon(127.13123D) + .build(); + } + + private Store createStoreEntity(Long userId, String storeName) { + return Store.builder() + .storeManagerId(userId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName(storeName) + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private StoreAddress createStoreAddressEntity(Store store, double lat, double lon) { + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + + return StoreAddress.builder() + .store(store) + .sido(sido) + .gugun(gugun) + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(lat) + .lon(lon) + .build(); + } + + private StoreAddress createStoresAddressWithSidoGugun(Store store, Sido sido, Gugun gugun) { + sidoRepository.save(sido); + gugunRepository.save(gugun); + return StoreAddress.builder() + .store(store) + .sido(sido) + .gugun(gugun) + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(0.0D) + .lon(0.0D) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/store/handler/GugunReaderTest.java b/src/test/java/kr/bb/store/domain/store/handler/GugunReaderTest.java new file mode 100644 index 0000000..b46910b --- /dev/null +++ b/src/test/java/kr/bb/store/domain/store/handler/GugunReaderTest.java @@ -0,0 +1,66 @@ +package kr.bb.store.domain.store.handler; + +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.GugunRepository; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.entity.address.SidoRepository; +import kr.bb.store.domain.store.exception.address.InvalidParentException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@SpringBootTest +@Transactional +class GugunReaderTest extends BasicIntegrationTestEnv { + @Autowired + private GugunReader gugunReader; + @Autowired + private SidoRepository sidoRepository; + @Autowired + private GugunRepository gugunRepository; + + @DisplayName("์‹œ/๋„, ๊ทธ๋ฆฌ๊ณ  ๊ตฌ/๊ตฐ๋ช…์„ ์ž…๋ ฅ๋ฐ›์•„ gugun๊ฐ’์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + public void readGugun() { + // given + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + + // when + Gugun gugunResult = gugunReader.readGugunCorrespondingSidoWithCode(sido, "110011"); + + // then + assertThat(gugunResult.getSido().getCode()).isEqualTo(sido.getCode()); + assertThat(gugunResult.getName()).isEqualTo("๊ฐ•๋‚จ๊ตฌ"); + } + + @DisplayName("ํ•ด๋‹น ์‹œ/๋„์— ํฌํ•จ๋˜์ง€ ์•Š๋Š” ๊ตฌ/๊ตฐ๋ช…์„ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์—†๋‹ค") + @Test + public void cannotReadWhenNotCorrespondingSidoAndGugun() { + // given + Sido sido1 = new Sido("011", "์„œ์šธ"); + Sido sido2 = new Sido("022", "์ˆ˜์›"); + sidoRepository.saveAll(List.of(sido1,sido2)); + Gugun gugun1 = new Gugun("110011",sido1,"๊ฐ•๋‚จ๊ตฌ"); + Gugun gugun2 = new Gugun("223322",sido2,"์˜ํ†ต๊ตฌ"); + gugunRepository.saveAll(List.of(gugun1,gugun2)); + + // when // then + assertThatThrownBy(() -> gugunReader.readGugunCorrespondingSidoWithCode(sido1,"223322")) + .isInstanceOf(InvalidParentException.class) + .hasMessage("์„ ํƒํ•œ ์‹œ/๋„์™€ ๊ตฌ/๊ตฐ์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/store/handler/StoreCreatorTest.java b/src/test/java/kr/bb/store/domain/store/handler/StoreCreatorTest.java new file mode 100644 index 0000000..bbb3e81 --- /dev/null +++ b/src/test/java/kr/bb/store/domain/store/handler/StoreCreatorTest.java @@ -0,0 +1,117 @@ +package kr.bb.store.domain.store.handler; + +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.store.controller.request.StoreCreateRequest; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.GugunRepository; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.entity.address.SidoRepository; +import kr.bb.store.domain.store.exception.CannotOwnMultipleStoreException; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.persistence.EntityManager; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class StoreCreatorTest extends BasicIntegrationTestEnv { + @Autowired + private StoreCreator storeCreator; + @Autowired + private StoreRepository storeRepository; + @Autowired + private SidoRepository sidoRepository; + @Autowired + private GugunRepository gugunRepository; + @Autowired + private EntityManager em; + + @DisplayName("ํšŒ์› ๋ฒˆํ˜ธ๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ๊ฐ€๊ฒŒ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + @Test + void createStore() { + // given + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + + Long userId = 1L; + StoreCreateRequest storeDto = createStoreRequest(); + + // when + Store store = storeCreator.create(userId, storeDto, sido, gugun); + + // then + assertThat(store.getId()).isNotNull(); + assertThat(store.getStoreManagerId()).isEqualTo(userId); + } + + @DisplayName("์ฒ˜์Œ ์ƒ์„ฑ๋œ ๊ฐ€๊ฒŒ์˜ ํ‰๊ท ๋ณ„์ ์€ null์ด ์•„๋‹Œ ๊ธฐ๋ณธ๊ฐ’์ด ๋“ค์–ด๊ฐ„๋‹ค") + @Test + void storeHasBasicProperties() { + // given + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + + Long userId = 1L; + StoreCreateRequest storeDto = createStoreRequest(); + + // when + Store store = storeCreator.create(userId, storeDto, sido, gugun); + em.flush(); + em.clear(); + + Store savedStore = storeRepository.findById(store.getId()).get(); + + // then + assertThat(savedStore.getAverageRating()).isNotNull().isEqualTo(0.0F); + + } + + @DisplayName("๊ฐ€๊ฒŒ์‚ฌ์žฅ์€ ๋‘˜ ์ด์ƒ์˜ ๊ฐ€๊ฒŒ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์—†๋‹ค") + @Test + void userCanCreateOnlyOneStore() { + // given + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + + Long userId = 1L; + StoreCreateRequest storeDto = createStoreRequest(); + + // when // then + assertThatThrownBy(() -> { + storeCreator.create(userId, storeDto, sido, gugun); + storeCreator.create(userId, storeDto, sido, gugun); + }).isInstanceOf(CannotOwnMultipleStoreException.class) + .hasMessage("๋‘˜ ์ด์ƒ์˜ ๊ฐ€๊ฒŒ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."); + + } + + private StoreCreateRequest createStoreRequest() { + return StoreCreateRequest.builder() + .storeName("๊ฐ€๊ฒŒ1") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .sido("์„œ์šธ") + .gugun("๊ฐ•๋‚จ๊ตฌ") + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(33.33322D) + .lon(127.13123D) + .deliveryPrice(5_000L) + .freeDeliveryMinPrice(10_000L) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/store/handler/StoreManagerTest.java b/src/test/java/kr/bb/store/domain/store/handler/StoreManagerTest.java new file mode 100644 index 0000000..7f3a70d --- /dev/null +++ b/src/test/java/kr/bb/store/domain/store/handler/StoreManagerTest.java @@ -0,0 +1,135 @@ +package kr.bb.store.domain.store.handler; + +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.store.controller.request.StoreInfoEditRequest; +import kr.bb.store.domain.store.entity.DeliveryPolicy; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.GugunRepository; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.entity.address.SidoRepository; +import kr.bb.store.domain.store.repository.DeliveryPolicyRepository; +import kr.bb.store.domain.store.repository.StoreAddressRepository; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.persistence.EntityManager; + +import static org.assertj.core.api.Assertions.assertThat; + +class StoreManagerTest extends BasicIntegrationTestEnv { + @Autowired + private StoreManager storeManager; + @Autowired + private StoreRepository storeRepository; + @Autowired + private StoreAddressRepository storeAddressRepository; + @Autowired + private DeliveryPolicyRepository deliveryPolicyRepository; + @Autowired + private SidoRepository sidoRepository; + @Autowired + private GugunRepository gugunRepository; + @Autowired + private EntityManager em; + + @DisplayName("์š”์ฒญ๋ฐ›์€ ๋‚ด์šฉ์œผ๋กœ ๊ฐ€๊ฒŒ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•œ๋‹ค - ๊ฐ€๊ฒŒ๋ช…, ์œ„๋„, ์ตœ์†Œ์ฃผ๋ฌธ๊ธˆ์•ก ์ˆ˜์ •") + @Test + public void editStore() { + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + + Long userId = 1L; + + Store store = createStore(userId); + storeRepository.save(store); + + StoreAddress storeAddress = createStoreAddress(store); + storeAddressRepository.save(storeAddress); + + DeliveryPolicy deliveryPolicy = createDeliveryPolicy(store); + deliveryPolicyRepository.save(deliveryPolicy); + + StoreInfoEditRequest storeEditRequest = StoreInfoEditRequest.builder() + .storeName("๊ฐ€๊ฒŒ2") // ์ˆ˜์ •๋จ + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .deliveryPrice(9_999L) // ์ˆ˜์ •๋จ + .freeDeliveryMinPrice(10_000L) + .sido("์„œ์šธ") + .gugun("๊ฐ•๋‚จ๊ตฌ") + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(-11.1111D) // ์ˆ˜์ •๋จ + .lon(127.13123D) + .build(); + em.flush(); + em.clear(); + + Store savedStore = storeRepository.findById(store.getId()).get(); + StoreAddress savedStoreAddress = storeAddressRepository.findByStoreId(store.getId()).get(); + DeliveryPolicy savedDeliveryPolicy = deliveryPolicyRepository.findByStoreId(store.getId()).get(); + + storeManager.edit(savedStore, savedStoreAddress, savedDeliveryPolicy, sido, gugun, storeEditRequest); + em.flush(); + em.clear(); + + Store changedStore = storeRepository.findById(store.getId()).get(); + StoreAddress changedStoreAddress = storeAddressRepository.findByStoreId(store.getId()).get(); + DeliveryPolicy changedDeliveryPolicy = deliveryPolicyRepository.findByStoreId(store.getId()).get(); + + assertThat(changedStore.getStoreName()).isEqualTo("๊ฐ€๊ฒŒ2"); + assertThat(changedStoreAddress.getLat()).isEqualTo(-11.1111D); + assertThat(changedDeliveryPolicy.getDeliveryPrice()).isEqualTo(9_999L); + + } + + private StoreAddress createStoreAddress(Store store) { + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + + return StoreAddress.builder() + .store(store) + .sido(sido) + .gugun(gugun) + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(33.33322D) + .lon(127.13123D) + .build(); + } + + private Store createStore(Long userId) { + return Store.builder() + .storeManagerId(userId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private DeliveryPolicy createDeliveryPolicy(Store store) { + return DeliveryPolicy.builder() + .store(store) + .deliveryPrice(5_000L) + .freeDeliveryMinPrice(10_000L) + .build(); + } + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/store/handler/StoreReaderTest.java b/src/test/java/kr/bb/store/domain/store/handler/StoreReaderTest.java new file mode 100644 index 0000000..c1ee38e --- /dev/null +++ b/src/test/java/kr/bb/store/domain/store/handler/StoreReaderTest.java @@ -0,0 +1,357 @@ +package kr.bb.store.domain.store.handler; + +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.store.controller.response.*; +import kr.bb.store.domain.store.dto.Position; +import kr.bb.store.domain.store.entity.DeliveryPolicy; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.GugunRepository; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.entity.address.SidoRepository; +import kr.bb.store.domain.store.exception.StoreNotFoundException; +import kr.bb.store.domain.store.repository.DeliveryPolicyRepository; +import kr.bb.store.domain.store.repository.StoreAddressRepository; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import javax.persistence.EntityManager; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; + +class StoreReaderTest extends BasicIntegrationTestEnv { + @Autowired + private StoreReader storeReader; + @Autowired + private StoreRepository storeRepository; + @Autowired + private StoreAddressRepository storeAddressRepository; + @Autowired + private DeliveryPolicyRepository deliveryPolicyRepository; + @Autowired + private SidoRepository sidoRepository; + @Autowired + private GugunRepository gugunRepository; + @Autowired + private EntityManager em; + + + @DisplayName("๊ฐ€๊ฒŒ ์•„์ด๋””๋ฅผ ์ž…๋ ฅ๋ฐ›์•„ ๊ฐ€๊ฒŒ์— ๋Œ€ํ•œ ์ƒ์„ธ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void readDetailInfo() { + Long userId = 1L; + + Store store = createStore(userId,"๊ฐ€๊ฒŒ1"); + storeRepository.save(store); + + StoreAddress storeAddress = createStoreAddress(store,0D,0D); + storeAddressRepository.save(storeAddress); + + DeliveryPolicy deliveryPolicy = createDeliveryPolicy(store); + deliveryPolicyRepository.save(deliveryPolicy); + + em.flush(); + em.clear(); + + // when + StoreDetailInfoResponse response = storeReader.readDetailInfo(store.getId()); + + // then + assertThat(response.getStoreName()).isEqualTo("๊ฐ€๊ฒŒ1"); + assertThat(response.getDeliveryPrice()).isEqualTo(5_000L); + assertThat(response.getSido()).isEqualTo("์„œ์šธ"); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ฐ€๊ฒŒ Id๋กœ ์š”์ฒญํ•˜๋ฉด ๊ฐ€๊ฒŒ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š๋Š”๋‹ค๋Š” ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + @Test + void cannotReadWhenUseInvalidStoreId() { + // given + Long storeId = 1L; + + // when + Assertions.assertThatThrownBy(() -> storeReader.readDetailInfo(storeId)) + .isInstanceOf(StoreNotFoundException.class) + .hasMessage("ํ•ด๋‹น ๊ฐ€๊ฒŒ๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + + } + + @DisplayName("ํŽ˜์ด์ง• ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด ๋ชจ๋“  ๊ฐ€๊ฒŒ ๋ฆฌ์ŠคํŠธ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void readStoresWithPaging() { + // given + Store s1 = createStore(1L,"๊ฐ€๊ฒŒ1"); + Store s2 = createStore(1L,"๊ฐ€๊ฒŒ1"); + Store s3 = createStore(1L,"๊ฐ€๊ฒŒ1"); + Store s4 = createStore(1L,"๊ฐ€๊ฒŒ1"); + Store s5 = createStore(1L,"๊ฐ€๊ฒŒ1"); + Store s6 = createStore(1L,"๊ฐ€๊ฒŒ1"); + Store s7 = createStore(1L,"๊ฐ€๊ฒŒ1"); + + storeRepository.saveAll(List.of(s1,s2,s3,s4,s5,s6,s7)); + + em.flush(); + em.clear(); + + int page = 1; + int size = 5; + Pageable pageable = PageRequest.of(page,size); + + // when + Page stores = storeReader.readStoresWithPaging(pageable); + + // then + assertThat(stores.getTotalPages()).isEqualTo(2); + assertThat(stores.getContent()).hasSize(2); + assertThat(stores.getTotalElements()).isEqualTo(7); + + } + + @DisplayName("์ผ๋ฐ˜ ๊ณ ๊ฐ์—๊ฒŒ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ€๊ฒŒ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค") + @Test + void readForUser() { + Long userId = 1L; + + Store store = createStore(userId,"๊ฐ€๊ฒŒ1"); + storeRepository.save(store); + + StoreAddress storeAddress = createStoreAddress(store,0D,0D); + storeAddressRepository.save(storeAddress); + + DeliveryPolicy deliveryPolicy = createDeliveryPolicy(store); + deliveryPolicyRepository.save(deliveryPolicy); + + em.flush(); + em.clear(); + + Boolean isLiked = true; + Boolean isSubscribed = true; + String subscriptionProductId = "๊ตฌ๋…์šฉ ์ƒํ’ˆ ์•„์ด๋””"; + + // when + StoreInfoUserResponse response = storeReader.readForUser(store.getId(),isLiked, isSubscribed, subscriptionProductId); + + // then + assertThat(response.getStoreName()).isEqualTo("๊ฐ€๊ฒŒ1"); + assertThat(response.getAverageRating()).isEqualTo(0.0D); + assertThat(response.getIsLiked()).isEqualTo(isLiked); + + } + @DisplayName("๊ฐ€๊ฒŒ ์‚ฌ์žฅ์—๊ฒŒ ๋ฐ˜ํ™˜ํ•˜๊ธฐ ์œ„ํ•œ ๊ฐ€๊ฒŒ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค") + @Test + void readForManager() { + Long userId = 1L; + + Store store = createStore(userId,"๊ฐ€๊ฒŒ1"); + storeRepository.save(store); + + StoreAddress storeAddress = createStoreAddress(store,0D,0D); + storeAddressRepository.save(storeAddress); + + DeliveryPolicy deliveryPolicy = createDeliveryPolicy(store); + deliveryPolicyRepository.save(deliveryPolicy); + + em.flush(); + em.clear(); + + // when + StoreInfoManagerResponse response = storeReader.readForManager(store.getId()); + + // then + assertThat(response.getStoreName()).isEqualTo("๊ฐ€๊ฒŒ1"); + assertThat(response.getAddress()).isEqualTo("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ"); + + } + + @DisplayName("์œ„/๊ฒฝ๋„๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ˜๊ฒฝ 2KM ์ด๋‚ด ๊ฐ€๊ฒŒ๋ฅผ ์ฐพ์•„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void getNearbyStores() { + // given + Double centerLat = 0.0D; + Double centerLon = 0.0D; + + Store s1 = createStore(1L,"๊ฐ€๊ฒŒ1"); + Store s2 = createStore(1L,"๊ฐ€๊ฒŒ2"); + Store s3 = createStore(1L,"๊ฐ€๊ฒŒ3"); + Store s4 = createStore(1L,"๊ฐ€๊ฒŒ4"); + Store s5 = createStore(1L,"๊ฐ€๊ฒŒ5"); + storeRepository.saveAll(List.of(s1,s2,s3,s4,s5)); + + StoreAddress sa1 = createStoreAddress(s1,0.0D, metersToLatitude(2000)); // ๋ฐ˜๊ฒฝ 2KM ์ด๋‚ด + StoreAddress sa2 = createStoreAddress(s2,0.0D, metersToLatitude(2001)); // ๋ฐ˜๊ฒฝ 2KM ์ด์™ธ + + StoreAddress sa3 = createStoreAddress(s3,metersToLatitude(-2000),0.0D); // ๋ฐ˜๊ฒฝ 2KM ์ด๋‚ด + StoreAddress sa4 = createStoreAddress(s4,metersToLatitude(-2001),0.0D); // ๋ฐ˜๊ฒฝ 2KM ์ด์™ธ + + StoreAddress sa5 = createStoreAddress(s5,100D,100D); // ๋ฐ˜๊ฒฝ 2KM ์ด์™ธ + storeAddressRepository.saveAll(List.of(sa1,sa2,sa3,sa4,sa5)); + + em.flush(); + em.clear(); + + // when + StoreListForMapResponse nearbyStores = storeReader.getNearbyStores(centerLat, centerLon, 5); + + // then + assertThat(nearbyStores.getStores()).hasSize(2); + assertThat(nearbyStores.getStores()).extracting("storeName","position") + .containsExactlyInAnyOrder( + tuple("๊ฐ€๊ฒŒ1",new Position(0.0D, metersToLatitude(2000))), + tuple("๊ฐ€๊ฒŒ3",new Position(metersToLatitude(-2000),0.0D)) + ); + + } + + @DisplayName("์‹œ์™€ ๊ตฐ์„ ์ž…๋ ฅ๋ฐ›์•„ ํ•ด๋‹น ์ง€์—ญ์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ€๊ฒŒ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค") + @Test + public void getStoresWithRegion() { + // given + Sido sido1 = new Sido("1", "์„œ์šธ"); + Sido sido2 = new Sido("2", "๋ถ€์‚ฐ"); + Gugun gugun1 = new Gugun("100",sido1,"๊ฐ•๋‚จ๊ตฌ"); + Gugun gugun2 = new Gugun("200",sido1,"์ข…๋กœ๊ตฌ"); + Gugun gugun3 = new Gugun("300",sido2,"๊ฑฐ์ฐฝ๊ตฐ"); + Gugun gugun4 = new Gugun("400",sido2,"์ฐฝ๋…•๊ตฐ"); + Gugun gugun5 = new Gugun("500",sido2,"์ง„ํ•ด๊ตฐ"); + + Store s1 = createStore(1L,"๊ฐ€๊ฒŒ1"); + Store s2 = createStore(1L,"๊ฐ€๊ฒŒ2"); + Store s3 = createStore(1L,"๊ฐ€๊ฒŒ3"); + Store s4 = createStore(1L,"๊ฐ€๊ฒŒ4"); + Store s5 = createStore(1L,"๊ฐ€๊ฒŒ5"); + storeRepository.saveAll(List.of(s1,s2,s3,s4,s5)); + + StoreAddress sa1 = createStoresAddressWithSidoGugun(s1, sido1, gugun1); + StoreAddress sa2 = createStoresAddressWithSidoGugun(s2, sido1, gugun2); + StoreAddress sa3 = createStoresAddressWithSidoGugun(s3, sido2, gugun3); + StoreAddress sa4 = createStoresAddressWithSidoGugun(s4, sido2, gugun4); + StoreAddress sa5 = createStoresAddressWithSidoGugun(s5, sido2, gugun5); + storeAddressRepository.saveAll(List.of(sa1,sa2,sa3,sa4,sa5)); + + em.flush(); + em.clear(); + + // when + StoreListForMapResponse storesWithRegion = storeReader.getStoresWithRegion(sido1, gugun1); + + // then + assertThat(storesWithRegion.getStores()).hasSize(1) + .extracting("storeName") + .containsExactly("๊ฐ€๊ฒŒ1"); + } + + @DisplayName("์‹œ๋งŒ ์ž…๋ ฅํ•˜๋ฉด ํ•ด๋‹น ์‹œ์— ํ•ด๋‹นํ•˜๋Š” ๊ฐ€๊ฒŒ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค") + @Test + public void getStoresWithRegionOnlySido() { + // given + Sido sido1 = new Sido("1", "์„œ์šธ"); + Sido sido2 = new Sido("2", "๋ถ€์‚ฐ"); + Gugun gugun1 = new Gugun("100",sido1,"๊ฐ•๋‚จ๊ตฌ"); + Gugun gugun2 = new Gugun("200",sido1,"์ข…๋กœ๊ตฌ"); + Gugun gugun3 = new Gugun("300",sido2,"ํ•ด์šด๋Œ€๊ตฌ"); + + Store s1 = createStore(1L,"๊ฐ€๊ฒŒ1"); + Store s2 = createStore(1L,"๊ฐ€๊ฒŒ2"); + Store s3 = createStore(1L,"๊ฐ€๊ฒŒ3"); + Store s4 = createStore(1L,"๊ฐ€๊ฒŒ4"); + Store s5 = createStore(1L,"๊ฐ€๊ฒŒ5"); + storeRepository.saveAll(List.of(s1,s2,s3,s4,s5)); + + StoreAddress sa1 = createStoresAddressWithSidoGugun(s1, sido1, gugun1); + StoreAddress sa2 = createStoresAddressWithSidoGugun(s2, sido1, gugun1); + StoreAddress sa3 = createStoresAddressWithSidoGugun(s3, sido1, gugun1); + StoreAddress sa4 = createStoresAddressWithSidoGugun(s4, sido1, gugun2); + StoreAddress sa5 = createStoresAddressWithSidoGugun(s5, sido2, gugun3); + storeAddressRepository.saveAll(List.of(sa1,sa2,sa3,sa4,sa5)); + + em.flush(); + em.clear(); + + // when + StoreListForMapResponse storesWithRegion = storeReader.getStoresWithRegion(sido1, null); + + // then + assertThat(storesWithRegion.getStores()).hasSize(4) + .extracting("storeName") + .containsExactlyInAnyOrder( + "๊ฐ€๊ฒŒ1","๊ฐ€๊ฒŒ2","๊ฐ€๊ฒŒ3","๊ฐ€๊ฒŒ4" + ); + + } + + + private StoreAddress createStoresAddressWithSidoGugun(Store store, Sido sido, Gugun gugun) { + sidoRepository.save(sido); + gugunRepository.save(gugun); + return StoreAddress.builder() + .store(store) + .sido(sido) + .gugun(gugun) + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(0.0D) + .lon(0.0D) + .build(); + } + + private StoreAddress createStoreAddress(Store store, double lat, double lon) { + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + + return StoreAddress.builder() + .store(store) + .sido(sido) + .gugun(gugun) + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(lat) + .lon(lon) + .build(); + } + + private Store createStore(Long userId, String storeName) { + return Store.builder() + .storeManagerId(userId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName(storeName) + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private DeliveryPolicy createDeliveryPolicy(Store store) { + return DeliveryPolicy.builder() + .store(store) + .deliveryPrice(5_000L) + .freeDeliveryMinPrice(10_000L) + .build(); + } + + private static double metersToLatitude(double meters) { + double latDiff = meters / 6371000.0; + return Math.toDegrees(latDiff); + } + + private static double metersToLongitude(double centerLat, double meters) { + double latRadians = Math.toRadians(centerLat); + double lonDiff = meters / (6371000.0 * Math.cos(latRadians)); + return Math.toDegrees(lonDiff); + } + + +} \ No newline at end of file diff --git a/src/test/java/kr/bb/store/domain/store/service/StoreServiceTest.java b/src/test/java/kr/bb/store/domain/store/service/StoreServiceTest.java new file mode 100644 index 0000000..9e72bfb --- /dev/null +++ b/src/test/java/kr/bb/store/domain/store/service/StoreServiceTest.java @@ -0,0 +1,740 @@ +package kr.bb.store.domain.store.service; + +import bloomingblooms.domain.flower.FlowerDto; +import bloomingblooms.domain.order.ValidatePriceDto; +import bloomingblooms.dto.command.StoreSettlementDto; +import kr.bb.store.client.dto.StoreNameAndAddressDto; +import kr.bb.store.domain.BasicIntegrationTestEnv; +import kr.bb.store.domain.store.controller.request.SortType; +import kr.bb.store.domain.store.controller.request.StoreCreateRequest; +import kr.bb.store.domain.store.controller.request.StoreInfoEditRequest; +import kr.bb.store.domain.store.controller.response.*; +import kr.bb.store.domain.store.entity.DeliveryPolicy; +import kr.bb.store.domain.store.entity.Store; +import kr.bb.store.domain.store.entity.StoreAddress; +import kr.bb.store.domain.store.entity.address.Gugun; +import kr.bb.store.domain.store.entity.address.GugunRepository; +import kr.bb.store.domain.store.entity.address.Sido; +import kr.bb.store.domain.store.entity.address.SidoRepository; +import kr.bb.store.domain.store.exception.DeliveryInconsistencyException; +import kr.bb.store.domain.store.exception.address.GugunNotFoundException; +import kr.bb.store.domain.store.exception.address.InvalidParentException; +import kr.bb.store.domain.store.exception.address.SidoNotFoundException; +import kr.bb.store.domain.store.repository.DeliveryPolicyRepository; +import kr.bb.store.domain.store.repository.StoreAddressRepository; +import kr.bb.store.domain.store.repository.StoreRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +import javax.persistence.EntityManager; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class StoreServiceTest extends BasicIntegrationTestEnv { + @Autowired + private StoreService storeService; + @Autowired + private StoreRepository storeRepository; + @Autowired + private SidoRepository sidoRepository; + @Autowired + private GugunRepository gugunRepository; + @Autowired + private StoreAddressRepository storeAddressRepository; + @Autowired + private DeliveryPolicyRepository deliveryPolicyRepository; + @Autowired + private EntityManager em; + + @DisplayName("ํšŒ์› ๋ฒˆํ˜ธ๋ฅผ ์ „๋‹ฌ๋ฐ›์•„ ๊ฐ€๊ฒŒ๋ฅผ ์ƒ์„ฑํ•œ๋‹ค") + @Test + void createStore() { + // given + Long userId = 1L; + StoreCreateRequest storeCreateRequest = createStoreCreateRequest(); + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + List flowers = Collections.emptyList(); + + // when + storeService.createStore(userId, storeCreateRequest, flowers); + em.flush(); + em.clear(); + + Store store = storeRepository.findByStoreManagerId(userId).get(); + // then + assertThat(store.getId()).isNotNull(); + assertThat(store.getStoreManagerId()).isEqualTo(userId); + } + + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ์‹œ/๋„ ์ •๋ณด๋กœ ๊ฐ€๊ฒŒ์ฃผ์†Œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์—†๋‹ค") + @Test + void cannotCreateStoreAddressWithoutSido() { + // given + StoreCreateRequest storeCreateRequest = createStoreCreateRequest(); + List flowers = Collections.emptyList(); + + // when // then + assertThatThrownBy(() -> storeService.createStore(1L, storeCreateRequest, flowers)) + .isInstanceOf(SidoNotFoundException.class) + .hasMessage("ํ•ด๋‹น ์‹œ/๋„๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + @DisplayName("์กด์žฌํ•˜์ง€ ์•Š๋Š” ๊ตฌ/๊ตฐ ์ •๋ณด๋กœ ๊ฐ€๊ฒŒ์ฃผ์†Œ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์—†๋‹ค") + @Test + void cannotCreateStoreAddressWithoutGugun() { + // given + Sido sido = new Sido("011", "์„œ์šธ"); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + sidoRepository.save(sido); + StoreCreateRequest storeCreateRequest = createStoreCreateRequest(); + Store store = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + List flowers = Collections.emptyList(); + + // when // then + assertThatThrownBy(() -> storeService.createStore(1L, storeCreateRequest, flowers)) + .isInstanceOf(GugunNotFoundException.class) + .hasMessage("ํ•ด๋‹น ๊ตฌ/๊ตฐ์ด ์กด์žฌํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + } + + + @DisplayName("์š”์ฒญ๋ฐ›์€ ๋‚ด์šฉ์œผ๋กœ ๊ฐ€๊ฒŒ ์ •๋ณด๋ฅผ ์ˆ˜์ •ํ•œ๋‹ค - ๊ฐ€๊ฒŒ๋ช… ์ˆ˜์ • ์˜ˆ์‹œ") + @Test + public void editStore() { + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + List flowers = Collections.emptyList(); + + + Long userId = 1L; + StoreCreateRequest request = createStoreCreateRequest(); + Long storeId = storeService.createStore(userId, request, flowers); + StoreInfoEditRequest storeEditRequest = StoreInfoEditRequest.builder() + .storeName("๊ฐ€๊ฒŒ2") // ์ˆ˜์ •๋จ + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .deliveryPrice(5_000L) + .freeDeliveryMinPrice(10_000L) + .sido("์„œ์šธ") + .gugun("๊ฐ•๋‚จ๊ตฌ") + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(33.33322D) + .lon(127.13123D) + .build(); + em.flush(); + em.clear(); + + storeService.editStoreInfo(storeId, storeEditRequest); + em.flush(); + em.clear(); + + Store changedStore = storeRepository.findById(storeId).get(); + + assertThat(changedStore.getStoreName()).isEqualTo("๊ฐ€๊ฒŒ2"); + } + + @DisplayName("๊ฐ€๊ฒŒ์•„์ด๋””๋ฅผ ํ†ตํ•ด ๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด๋ฅผ ๋ฐ›์•„์˜จ๋‹ค") + @Test + void getStoreInfo() { + // given + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + List flowers = Collections.emptyList(); + + Long userId = 1L; + StoreCreateRequest request = createStoreCreateRequest(); + Long storeId = storeService.createStore(userId, request, flowers); + em.flush(); + em.clear(); + + // when + StoreDetailInfoResponse response = storeService.getStoreDetailInfo(storeId); + + // then + assertThat(response.getStoreName()).isEqualTo("๊ฐ€๊ฒŒ1"); + assertThat(response.getDeliveryPrice()).isEqualTo(5000L); + assertThat(response.getSido()).isEqualTo("์„œ์šธ"); + } + + @DisplayName("์‚ฌ์šฉ์ž๊ฐ€ ์š”์ฒญํ•œ ๊ฐ€๊ฒŒ ํŽ˜์ด์ง• ๋ฐ์ดํ„ฐ๋ฅผ ํ•„์š”ํ•œ ๊ฐ’๋งŒ ๋‹ด์•„์„œ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + public void getStoresWithPaing() { + // given + Store s1 = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + Store s2 = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + Store s3 = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + Store s4 = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + Store s5 = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + Store s6 = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + Store s7 = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + + storeRepository.saveAll(List.of(s1,s2,s3,s4,s5,s6,s7)); + + em.flush(); + em.clear(); + + int page = 1; + int size = 5; + + Pageable pageable = PageRequest.of(page,size); + + // when + Page response = storeService.getStoresWithPaging(pageable); + + // then + assertThat(response.getTotalElements()).isEqualTo(7); + assertThat(response.getContent().get(0)).isInstanceOf(StoreListResponse.class); + } + + @DisplayName("์œ ์ €์—๊ฒŒ ๋ณด์ด๋Š” ๊ฐ€๊ฒŒ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + public void getStoreInfoForUser() { + Long userId = 1L; + + Store store = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + storeRepository.save(store); + + StoreAddress storeAddress = createStoreAddressEntity(store,0D,0D); + storeAddressRepository.save(storeAddress); + + DeliveryPolicy deliveryPolicy = createDeliveryPolicyEntity(store); + deliveryPolicyRepository.save(deliveryPolicy); + + String subscriptionProductId = "1"; + + em.flush(); + em.clear(); + + // when + StoreInfoUserResponse response = storeService.getStoreInfoForUser(store.getId(), false, false, subscriptionProductId); + + // then + assertThat(response.getStoreName()).isEqualTo("๊ฐ€๊ฒŒ1"); + assertThat(response.getAverageRating()).isEqualTo(0.0D); + } + + @DisplayName("๊ฐ€๊ฒŒ ์‚ฌ์žฅ์—๊ฒŒ ๋ณด์ด๋Š” ๊ฐ€๊ฒŒ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + public void getStoreInfoForManager() { + Long userId = 1L; + + Store store = createStoreEntity(userId,"๊ฐ€๊ฒŒ1"); + storeRepository.save(store); + + StoreAddress storeAddress = createStoreAddressEntity(store,0D,0D); + storeAddressRepository.save(storeAddress); + + DeliveryPolicy deliveryPolicy = createDeliveryPolicyEntity(store); + deliveryPolicyRepository.save(deliveryPolicy); + + em.flush(); + em.clear(); + + // when + StoreInfoManagerResponse response = storeService.getStoreInfoForManager(store.getId()); + + // then + assertThat(response.getStoreName()).isEqualTo("๊ฐ€๊ฒŒ1"); + assertThat(response.getAddress()).isEqualTo("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ"); + + } + + @DisplayName("์‹œ/๋„ ์ด๋ฆ„๊ณผ ๊ตฌ/๊ตฐ ์ด๋ฆ„์„ ํ†ตํ•ด ๊ฐ€๊ฒŒ๋ฅผ ๊ฒ€์ƒ‰ํ•œ๋‹ค") + @Test + void getStoresWithRegion() { + // given + Sido sido1 = new Sido("1", "์„œ์šธ"); + Sido sido2 = new Sido("2", "๋ถ€์‚ฐ"); + Gugun gugun1 = new Gugun("100",sido1,"๊ฐ•๋‚จ๊ตฌ"); + Gugun gugun2 = new Gugun("200",sido1,"์ข…๋กœ๊ตฌ"); + + Store s1 = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + Store s2 = createStoreEntity(1L,"๊ฐ€๊ฒŒ2"); + Store s3 = createStoreEntity(1L,"๊ฐ€๊ฒŒ3"); + Store s4 = createStoreEntity(1L,"๊ฐ€๊ฒŒ4"); + Store s5 = createStoreEntity(1L,"๊ฐ€๊ฒŒ5"); + storeRepository.saveAll(List.of(s1,s2,s3,s4,s5)); + + StoreAddress sa1 = createStoresAddressWithSidoGugun(s1, sido1, gugun1); + StoreAddress sa2 = createStoresAddressWithSidoGugun(s2, sido1, gugun1); + StoreAddress sa3 = createStoresAddressWithSidoGugun(s3, sido1, gugun2); + StoreAddress sa4 = createStoresAddressWithSidoGugun(s4, sido1, gugun2); + StoreAddress sa5 = createStoresAddressWithSidoGugun(s5, sido1, gugun2); + storeAddressRepository.saveAll(List.of(sa1,sa2,sa3,sa4,sa5)); + + em.flush(); + em.clear(); + + StoreListForMapResponse storesWithRegion = storeService.getStoresWithRegion(sido1.getCode(), gugun1.getCode()); + assertThat(storesWithRegion.getStores()).hasSize(2) + .extracting("storeName") + .containsExactlyInAnyOrder( + "๊ฐ€๊ฒŒ1","๊ฐ€๊ฒŒ2" + ); + } + + + @DisplayName("์ง€์—ญ์œผ๋กœ ๊ฒ€์ƒ‰ํ•  ๋•Œ ์‹œ/๋„ ๊ฐ’์€ ํ•„์ˆ˜๋กœ ์ž…๋ ฅํ•ด์•ผ ํ•œ๋‹ค") + @Test + void sidoMustNotBeNullWhenGetStoresWithRegion() { + // when // then + assertThatThrownBy(() -> storeService.getStoresWithRegion(null,"๊ฐ•๋‚จ๊ตฌ")) + .isInstanceOf(InvalidDataAccessApiUsageException.class); + + } + @DisplayName("์‹œ์— ๋งž์ง€ ์•Š๋Š” ๊ตฐ์„ ์ž…๋ ฅํ•˜๋ฉด ์—๋Ÿฌ๊ฐ€ ๋ฐœ์ƒํ•œ๋‹ค") + @Test + void gugunHasRightSidoWhenGetStoresWithRegion() { + // given + Sido sido1 = new Sido("1", "์„œ์šธ"); + Sido sido2 = new Sido("2", "๋ถ€์‚ฐ"); + Gugun gugun1 = new Gugun("300",sido2,"ํ•ด์šด๋Œ€๊ตฌ"); + sidoRepository.saveAll(List.of(sido1, sido2)); + gugunRepository.save(gugun1); + + // when // then + assertThatThrownBy(() -> storeService.getStoresWithRegion(sido1.getCode(),gugun1.getCode())) + .isInstanceOf(InvalidParentException.class) + .hasMessage("์„ ํƒํ•œ ์‹œ/๋„์™€ ๊ตฌ/๊ตฐ์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค."); + + } + @DisplayName("๊ตฐ์„ ์ž…๋ ฅํ•˜์ง€ ์•Š์œผ๋ฉด ์‹œ์— ํ•ด๋‹นํ•˜๋Š” ๋ชจ๋“  ๊ฐ€๊ฒŒ์ •๋ณด๊ฐ€ ๋ฐ˜ํ™˜๋œ๋‹ค") + @Test + void getStoresWithRegionReadAllSidoWhenGugunIsBlank() { + // given + Sido sido1 = new Sido("1", "์„œ์šธ"); + Sido sido2 = new Sido("2", "๋ถ€์‚ฐ"); + Gugun gugun1 = new Gugun("100",sido1,"๊ฐ•๋‚จ๊ตฌ"); + Gugun gugun2 = new Gugun("200",sido1,"์ข…๋กœ๊ตฌ"); + Gugun gugun3 = new Gugun("300",sido2,"ํ•ด์šด๋Œ€๊ตฌ"); + + Store s1 = createStoreEntity(1L,"๊ฐ€๊ฒŒ1"); + Store s2 = createStoreEntity(1L,"๊ฐ€๊ฒŒ2"); + Store s3 = createStoreEntity(1L,"๊ฐ€๊ฒŒ3"); + Store s4 = createStoreEntity(1L,"๊ฐ€๊ฒŒ4"); + Store s5 = createStoreEntity(1L,"๊ฐ€๊ฒŒ5"); + storeRepository.saveAll(List.of(s1,s2,s3,s4,s5)); + + StoreAddress sa1 = createStoresAddressWithSidoGugun(s1, sido1, gugun1); + StoreAddress sa2 = createStoresAddressWithSidoGugun(s2, sido1, gugun1); + StoreAddress sa3 = createStoresAddressWithSidoGugun(s3, sido1, gugun1); + StoreAddress sa4 = createStoresAddressWithSidoGugun(s4, sido1, gugun2); + StoreAddress sa5 = createStoresAddressWithSidoGugun(s5, sido2, gugun3); + storeAddressRepository.saveAll(List.of(sa1,sa2,sa3,sa4,sa5)); + + em.flush(); + em.clear(); + + StoreListForMapResponse storesWithRegion = storeService.getStoresWithRegion(sido1.getCode(), ""); + assertThat(storesWithRegion.getStores()).hasSize(4) + .extracting("storeName") + .containsExactlyInAnyOrder( + "๊ฐ€๊ฒŒ1","๊ฐ€๊ฒŒ2","๊ฐ€๊ฒŒ3","๊ฐ€๊ฒŒ4" + ); + + } + + @DisplayName("๊ฐ€๊ฒŒ์‚ฌ์žฅ ์•„์ด๋””๋ฅผ ํ†ตํ•ด ๊ฐ€๊ฒŒ ์•„์ด๋””๋ฅผ ๊ฐ€์ ธ์˜จ๋‹ค") + @Test + void getStoreId() { + // given + Long userId = 1L; + Store store = createStoreWithManagerId(userId); + storeRepository.save(store); + + // when + Long result = storeService.getStoreId(userId); + + // then + assertThat(result).isEqualTo(store.getId()); + + } + + @DisplayName("๊ฐ€๊ฒŒ ์ด๋ฆ„๊ณผ ์ฃผ์†Œ๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void getStoreNameAndAddress() { + // given + String storeName = "์šฐ๋ฆฌ๊ฐ€๊ฒŒ"; + Store store = createStoreWithStoreName(storeName); + storeRepository.save(store); + + StoreAddress storeAddress = createStoreAddressEntity(store,"๋„๋กœ๋ช… ์ฃผ์†Œ", "์ƒ์„ธ์ฃผ์†Œ"); + storeAddressRepository.save(storeAddress); + + // when + StoreNameAndAddressDto result = storeService.getStoreNameAndAddress(store.getId()); + + // then + assertThat(result.getStoreName()).isEqualTo(storeName); + assertThat(result.getStoreAddress()).isEqualTo(storeAddress.getAddress() + " " + storeAddress.getDetailAddress()); + + } + + @DisplayName("์ตœ์†Œ ์ฃผ๋ฌธ ๊ธˆ์•ก์„ ๋งŒ์กฑํ•˜๋ฉด ๋ฐฐ๋‹ฌ๋น„๋Š” ๋ฌด๋ฃŒ๋‹ค") + @Test + void validateDeliveryPrice() { + // given + Store store = createStoreEntity(); + storeRepository.save(store); + + Long deliveryPrice = 10_000L; + Long freeDeliveryMinPrice = 100_000L; + DeliveryPolicy deliveryPolicy = createDeliveryPolicyEntity(store, deliveryPrice, freeDeliveryMinPrice); + deliveryPolicyRepository.save(deliveryPolicy); + + ValidatePriceDto validatePriceDto = ValidatePriceDto.builder() + .storeId(store.getId()) + .actualAmount(100_001L) + .deliveryCost(10_000L) + .build(); + + // when // then + assertThatThrownBy(() -> storeService.validateDeliveryPrice(List.of(validatePriceDto))) + .isInstanceOf(DeliveryInconsistencyException.class) + .hasMessage("์ฃผ๋ฌธ ์š”์ฒญ์ด ๋ฐฐ์†ก ์ •์ฑ…์„ ์œ„๋ฐ˜ํ–ˆ์Šต๋‹ˆ๋‹ค."); + + } + + @DisplayName("์ตœ์†Œ ์ฃผ๋ฌธ ๊ธˆ์•ก์„ ๋งŒ์กฑํ•˜์ง€ ๋ชปํ–ˆ์œผ๋ฉด ๋ฐฐ๋‹ฌ๋น„๋ฅผ ์ง€๋ถˆํ•ด์•ผ ํ•œ๋‹ค") + @Test + void validateDeliveryPrice2() { + // given + Store store = createStoreEntity(); + storeRepository.save(store); + + Long deliveryPrice = 10_000L; + Long freeDeliveryMinPrice = 100_000L; + DeliveryPolicy deliveryPolicy = createDeliveryPolicyEntity(store, deliveryPrice, freeDeliveryMinPrice); + deliveryPolicyRepository.save(deliveryPolicy); + + ValidatePriceDto validatePriceDto = ValidatePriceDto.builder() + .storeId(store.getId()) + .actualAmount(99_999L) + .deliveryCost(0L) + .build(); + + // when // then + assertThatThrownBy(() -> storeService.validateDeliveryPrice(List.of(validatePriceDto))) + .isInstanceOf(DeliveryInconsistencyException.class) + .hasMessage("์ฃผ๋ฌธ ์š”์ฒญ์ด ๋ฐฐ์†ก ์ •์ฑ…์„ ์œ„๋ฐ˜ํ–ˆ์Šต๋‹ˆ๋‹ค."); + + } + + @DisplayName("๊ฐ€๊ฒŒ์˜ ํ‰๊ท ํ‰์ ์„ ์—…๋ฐ์ดํŠธํ•œ๋‹ค") + @Test + void updateAverageRating() { + // given + Store s1 = createStoreEntity(); + Store s2 = createStoreEntity(); + Store s3 = createStoreEntity(); + + storeRepository.saveAll(List.of(s1, s2, s3)); + em.flush(); + em.clear(); + + Map averageRatings = Map.of(s1.getId(), 5D, s2.getId(), 4.2D, s3.getId(), 4.8D); + + // when + storeService.updateAverageRating(averageRatings); + em.flush(); + em.clear(); + + List result = storeRepository.findAll(); + + // then + assertThat(result).extracting("averageRating") + .containsExactlyInAnyOrder(5D,4.2D,4.8D); + + } + @DisplayName("๊ฐ€๊ฒŒ์˜ ์›” ๋งค์ถœ์•ก์„ ์—…๋ฐ์ดํŠธํ•œ๋‹ค") + @Test + void updateMonthlySalesRevenue() { + // given + Store s1 = createStoreEntity(); + Store s2 = createStoreEntity(); + Store s3 = createStoreEntity(); + + storeRepository.saveAll(List.of(s1, s2, s3)); + em.flush(); + em.clear(); + + List monthlySalesRevenues = List.of(new StoreSettlementDto(s1.getId(),100_000L), + new StoreSettlementDto(s2.getId(),200_000L), + new StoreSettlementDto(s3.getId(),300_000L)); + + // when + storeService.updateMonthlySalesRevenue(monthlySalesRevenues); + em.flush(); + em.clear(); + + List result = storeRepository.findAll(); + + // then + assertThat(result).extracting("monthlySalesRevenue") + .containsExactlyInAnyOrder(100_000L,200_000L,300_000L); + + } + + @DisplayName("๊ด€๋ฆฌ์ž์˜ ๊ฐ€๊ฒŒ ์กฐํšŒ ์‹œ gugun์€ ์„ ํƒํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ๋‹ค") + @Test + void getStoresForAdmin1() { + // given + Sido sido1 = new Sido("1", "์„œ์šธ"); + Sido sido2 = new Sido("2", "๋ถ€์‚ฐ"); + Gugun gugun1 = new Gugun("100",sido1,"๊ฐ•๋‚จ๊ตฌ"); + Gugun gugun2 = new Gugun("200",sido1,"์ข…๋กœ๊ตฌ"); + Gugun gugun3 = new Gugun("300",sido2,"ํ•ด์šด๋Œ€๊ตฌ"); + + Store s1 = createStoreWithManagerId(1L); + Store s2 = createStoreWithManagerId(2L); + Store s3 = createStoreWithManagerId(3L); + Store s4 = createStoreWithManagerId(4L); + Store s5 = createStoreWithManagerId(5L); + storeRepository.saveAll(List.of(s1,s2,s3,s4,s5)); + + StoreAddress sa1 = createStoresAddressWithSidoGugun(s1,sido1,gugun1); + StoreAddress sa2 = createStoresAddressWithSidoGugun(s2,sido1,gugun2); + StoreAddress sa3 = createStoresAddressWithSidoGugun(s3,sido2,gugun3); + StoreAddress sa4 = createStoresAddressWithSidoGugun(s4,sido1,gugun1); + StoreAddress sa5 = createStoresAddressWithSidoGugun(s5,sido1,gugun1); + storeAddressRepository.saveAll(List.of(sa1,sa2,sa3,sa4,sa5)); + + Pageable page = PageRequest.of(0,5); + SortType sort = SortType.DATE; + String sidoCode = sido1.getCode(); + String gugunCode = ""; + + // when + List result = storeService.getStoresForAdmin(page, sort, sidoCode, gugunCode).getContent(); + + // then + assertThat(result).hasSize(4) + .extracting("storeManagerId") + .containsExactlyInAnyOrder(5L,4L,2L,1L); + + } + + @DisplayName("๊ด€๋ฆฌ์ž์˜ ๊ฐ€๊ฒŒ ์กฐํšŒ ์‹œ ์ •๋ ฌ์กฐ๊ฑด์„ ์„ ํƒํ•˜์ง€ ์•Š์œผ๋ฉด ๋‚ ์งœ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ๋‹ค") + @Test + void getStoresForAdmin2() { + // given + Sido sido1 = new Sido("1", "์„œ์šธ"); + Sido sido2 = new Sido("2", "๋ถ€์‚ฐ"); + Gugun gugun1 = new Gugun("100",sido1,"๊ฐ•๋‚จ๊ตฌ"); + Gugun gugun2 = new Gugun("200",sido1,"์ข…๋กœ๊ตฌ"); + Gugun gugun3 = new Gugun("300",sido2,"ํ•ด์šด๋Œ€๊ตฌ"); + + Store s1 = createStoreWithManagerId(1L); + Store s2 = createStoreWithManagerId(2L); + Store s3 = createStoreWithManagerId(3L); + Store s4 = createStoreWithManagerId(4L); + Store s5 = createStoreWithManagerId(5L); + storeRepository.saveAll(List.of(s1,s2,s3,s4,s5)); + + StoreAddress sa1 = createStoresAddressWithSidoGugun(s1,sido1,gugun1); + StoreAddress sa2 = createStoresAddressWithSidoGugun(s2,sido1,gugun2); + StoreAddress sa3 = createStoresAddressWithSidoGugun(s3,sido2,gugun3); + StoreAddress sa4 = createStoresAddressWithSidoGugun(s4,sido1,gugun1); + StoreAddress sa5 = createStoresAddressWithSidoGugun(s5,sido1,gugun1); + storeAddressRepository.saveAll(List.of(sa1,sa2,sa3,sa4,sa5)); + + Pageable page = PageRequest.of(0,5); + SortType sort = null; + String sidoCode = sido1.getCode(); + String gugunCode = gugun1.getCode(); + + // when + List result = storeService.getStoresForAdmin(page, sort, sidoCode, gugunCode).getContent(); + + // then + assertThat(result).hasSize(3); + IntStream.range(1, result.size()).forEach(i -> { + Store prev = result.get(i-1); + Store next = result.get(i); + assertThat(prev.getCreatedAt().isAfter(next.getCreatedAt()) || prev.getCreatedAt().isEqual(next.getCreatedAt())).isTrue(); + }); + } + + @DisplayName("์—ฌ๋Ÿฌ ๊ฐ€๊ฒŒ๋“ค์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•œ๋‹ค") + @Test + void getStoreNames() { + // given + Store s1 = createStoreWithStoreName("๊ฐ€๊ฒŒ1"); + Store s2 = createStoreWithStoreName("๊ฐ€๊ฒŒ2"); + Store s3 = createStoreWithStoreName("๊ฐ€๊ฒŒ3"); + List stores = storeRepository.saveAll(List.of(s1, s2, s3)); + List storeIds = stores.stream() + .map(Store::getId) + .collect(Collectors.toList()); + + // when + Map result = storeService.getStoreNames(storeIds); + + // then + assertThat(result.get(s1.getId())).isEqualTo(s1.getStoreName()); + assertThat(result.get(s2.getId())).isEqualTo(s2.getStoreName()); + assertThat(result.get(s3.getId())).isEqualTo(s3.getStoreName()); + + } + + private Store createStoreWithManagerId(Long userId) { + return Store.builder() + .storeManagerId(userId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ์ด๋ฆ„") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + private Store createStoreWithStoreName(String storeName) { + return Store.builder() + .storeManagerId(1L) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName(storeName) + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private StoreAddress createStoresAddressWithSidoGugun(Store store, Sido sido, Gugun gugun) { + sidoRepository.save(sido); + gugunRepository.save(gugun); + return StoreAddress.builder() + .store(store) + .sido(sido) + .gugun(gugun) + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(0.0D) + .lon(0.0D) + .build(); + } + + private StoreCreateRequest createStoreCreateRequest() { + return StoreCreateRequest.builder() + .storeName("๊ฐ€๊ฒŒ1") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .deliveryPrice(5_000L) + .freeDeliveryMinPrice(10_000L) + .sido("์„œ์šธ") + .gugun("๊ฐ•๋‚จ๊ตฌ") + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(33.333220D) + .lon(127.13123D) + .build(); + } + + private StoreAddress createStoreAddressEntity(Store store, double lat, double lon) { + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + + return StoreAddress.builder() + .store(store) + .sido(sido) + .gugun(gugun) + .address("์„œ์šธ ๊ฐ•๋‚จ๊ตฌ ๋‚จ๋ถ€์ˆœํ™˜๋กœ") + .detailAddress("202ํ˜ธ") + .zipCode("001112") + .lat(lat) + .lon(lon) + .build(); + } + private StoreAddress createStoreAddressEntity(Store store, String address, String detailAddress) { + Sido sido = new Sido("011", "์„œ์šธ"); + sidoRepository.save(sido); + Gugun gugun = new Gugun("110011",sido,"๊ฐ•๋‚จ๊ตฌ"); + gugunRepository.save(gugun); + + return StoreAddress.builder() + .store(store) + .sido(sido) + .gugun(gugun) + .address(address) + .detailAddress(detailAddress) + .zipCode("001112") + .lat(0d) + .lon(0d) + .build(); + } + + private Store createStoreEntity(Long userId, String storeName) { + return Store.builder() + .storeManagerId(userId) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName(storeName) + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + private DeliveryPolicy createDeliveryPolicyEntity(Store store) { + return DeliveryPolicy.builder() + .store(store) + .deliveryPrice(5_000L) + .freeDeliveryMinPrice(10_000L) + .build(); + } + private DeliveryPolicy createDeliveryPolicyEntity(Store store, Long deliveryPrice, Long freeDeliveryMinPrice) { + return DeliveryPolicy.builder() + .store(store) + .deliveryPrice(deliveryPrice) + .freeDeliveryMinPrice(freeDeliveryMinPrice) + .build(); + } + + private Store createStoreEntity() { + return Store.builder() + .storeManagerId(1L) + .storeCode("๊ฐ€๊ฒŒ์ฝ”๋“œ") + .storeName("๊ฐ€๊ฒŒ") + .detailInfo("๊ฐ€๊ฒŒ ์ƒ์„ธ์ •๋ณด") + .storeThumbnailImage("๊ฐ€๊ฒŒ ์ธ๋„ค์ผ") + .phoneNumber("๊ฐ€๊ฒŒ ์ „ํ™”๋ฒˆํ˜ธ") + .accountNumber("๊ฐ€๊ฒŒ ๊ณ„์ขŒ์ •๋ณด") + .bank("๊ฐ€๊ฒŒ ๊ณ„์ขŒ ์€ํ–‰์ •๋ณด") + .build(); + } + + +} \ No newline at end of file diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml new file mode 100644 index 0000000..96f0dc4 --- /dev/null +++ b/src/test/resources/application.yml @@ -0,0 +1,51 @@ +spring: + datasource: + url: jdbc:h2:mem:~/orderService + driver-class-name: org.h2.Driver + username: sa + password: + jpa: + hibernate: + ddl-auto: create + show-sql: true + properties: + hibernate: + format_sql: true + cloud: + config: + enabled: false + sql: + init: + mode: never + data: + redis: + host: localhost + port: 6379 + password: +cloud: + aws: + stack: + auto: false + region: + static: ap-northeast-1 + credentials: + ACCESS_KEY_ID: "test" + SECRET_ACCESS_KEY: "test" + sqs: + question-register-notification-queue: + url: + inquery-response-notification-queue: + url: + new-order-status-queue: + url: + out-of-stock-notification-queue: + url: +redisson: + lock: + wait-second: 5 + lease-second: 1 +endpoint: + product-service: localhost:8800 + storeLike-service: localhost:8500 + storeSubscription-service: localhost:9900 + user-service: localhost:8600 \ No newline at end of file