generated from pricelees/issue-pr-template
[#50] Tosspay API Mocking 서버 구현 #51
4
.gitignore
vendored
4
.gitignore
vendored
@ -37,3 +37,7 @@ out/
|
|||||||
.vscode/
|
.vscode/
|
||||||
logs
|
logs
|
||||||
.kotlin
|
.kotlin
|
||||||
|
|
||||||
|
### sql
|
||||||
|
data/*.sql
|
||||||
|
data/*.txt
|
||||||
@ -1,10 +1,9 @@
|
|||||||
dependencies {
|
dependencies {
|
||||||
|
api("net.ttddyy.observation:datasource-micrometer-spring-boot:1.1.2")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.20.0")
|
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.20.0")
|
||||||
implementation("net.ttddyy.observation:datasource-micrometer-spring-boot:1.1.2")
|
implementation(project(":common:utils"))
|
||||||
|
|
||||||
testImplementation("io.mockk:mockk:1.14.4")
|
testImplementation("io.mockk:mockk:1.14.4")
|
||||||
|
|
||||||
implementation(project(":common:utils"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named<Jar>("jar") {
|
tasks.named<Jar>("jar") {
|
||||||
|
|||||||
@ -7,17 +7,17 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
api("org.springframework.boot:spring-boot-starter-data-jpa")
|
||||||
implementation("com.github.f4b6a3:tsid-creator:5.2.6")
|
api("com.github.f4b6a3:tsid-creator:5.2.6")
|
||||||
|
|
||||||
|
implementation(project(":common:utils"))
|
||||||
|
implementation(project(":common:types"))
|
||||||
|
|
||||||
testRuntimeOnly("com.h2database:h2")
|
testRuntimeOnly("com.h2database:h2")
|
||||||
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
|
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
|
||||||
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
|
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
|
||||||
testImplementation("io.mockk:mockk:1.14.4")
|
testImplementation("io.mockk:mockk:1.14.4")
|
||||||
|
|
||||||
implementation(project(":common:utils"))
|
|
||||||
implementation(project(":common:types"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named<BootJar>("bootJar") {
|
tasks.named<BootJar>("bootJar") {
|
||||||
|
|||||||
@ -7,16 +7,16 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
api("org.springframework.boot:spring-boot-starter-web")
|
||||||
implementation("org.springframework.boot:spring-boot-starter-aop")
|
api("org.springframework.boot:spring-boot-starter-aop")
|
||||||
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
|
api("com.fasterxml.jackson.module:jackson-module-kotlin")
|
||||||
|
|
||||||
|
api(project(":common:log"))
|
||||||
|
api(project(":common:utils"))
|
||||||
|
api(project(":common:types"))
|
||||||
|
|
||||||
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
|
testImplementation("io.kotest:kotest-runner-junit5:5.9.1")
|
||||||
testImplementation("io.mockk:mockk:1.14.4")
|
testImplementation("io.mockk:mockk:1.14.4")
|
||||||
|
|
||||||
implementation(project(":common:log"))
|
|
||||||
implementation(project(":common:utils"))
|
|
||||||
implementation(project(":common:types"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.named<BootJar>("bootJar") {
|
tasks.named<BootJar>("bootJar") {
|
||||||
|
|||||||
@ -5,16 +5,10 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
// Spring
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
|
|
||||||
implementation("org.springframework.boot:spring-boot-starter-validation")
|
|
||||||
|
|
||||||
// API docs
|
// API docs
|
||||||
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")
|
implementation("org.springdoc:springdoc-openapi-starter-webmvc-ui:2.8.13")
|
||||||
|
|
||||||
// DB
|
// DB
|
||||||
implementation("com.github.f4b6a3:tsid-creator:5.2.6")
|
|
||||||
runtimeOnly("com.h2database:h2")
|
runtimeOnly("com.h2database:h2")
|
||||||
runtimeOnly("com.mysql:mysql-connector-j")
|
runtimeOnly("com.mysql:mysql-connector-j")
|
||||||
|
|
||||||
@ -24,7 +18,6 @@ dependencies {
|
|||||||
// Logging
|
// Logging
|
||||||
implementation("net.logstash.logback:logstash-logback-encoder:8.1")
|
implementation("net.logstash.logback:logstash-logback-encoder:8.1")
|
||||||
implementation("com.github.loki4j:loki-logback-appender:2.0.0")
|
implementation("com.github.loki4j:loki-logback-appender:2.0.0")
|
||||||
implementation("net.ttddyy.observation:datasource-micrometer-spring-boot:1.1.1")
|
|
||||||
|
|
||||||
// Observability
|
// Observability
|
||||||
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
@ -52,9 +45,6 @@ dependencies {
|
|||||||
|
|
||||||
// submodules
|
// submodules
|
||||||
implementation(project(":common:persistence"))
|
implementation(project(":common:persistence"))
|
||||||
implementation(project(":common:utils"))
|
|
||||||
implementation(project(":common:types"))
|
|
||||||
implementation(project(":common:log"))
|
|
||||||
implementation(project(":common:web"))
|
implementation(project(":common:web"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.sangdol.roomescape.payment.infrastructure.client
|
package com.sangdol.roomescape.payment.infrastructure.client
|
||||||
|
|
||||||
|
import io.micrometer.observation.ObservationRegistry
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
import org.springframework.boot.context.properties.EnableConfigurationProperties
|
||||||
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder
|
import org.springframework.boot.http.client.ClientHttpRequestFactoryBuilder
|
||||||
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings
|
import org.springframework.boot.http.client.ClientHttpRequestFactorySettings
|
||||||
@ -17,6 +18,7 @@ class PaymentConfig {
|
|||||||
@Bean
|
@Bean
|
||||||
fun tosspayClientBuilder(
|
fun tosspayClientBuilder(
|
||||||
paymentProperties: PaymentProperties,
|
paymentProperties: PaymentProperties,
|
||||||
|
observationRegistry: ObservationRegistry
|
||||||
): RestClient.Builder {
|
): RestClient.Builder {
|
||||||
val settings: ClientHttpRequestFactorySettings = ClientHttpRequestFactorySettings.defaults().also {
|
val settings: ClientHttpRequestFactorySettings = ClientHttpRequestFactorySettings.defaults().also {
|
||||||
it.withReadTimeout(Duration.ofSeconds(paymentProperties.readTimeout.toLong()))
|
it.withReadTimeout(Duration.ofSeconds(paymentProperties.readTimeout.toLong()))
|
||||||
@ -26,6 +28,7 @@ class PaymentConfig {
|
|||||||
|
|
||||||
return RestClient.builder()
|
return RestClient.builder()
|
||||||
.baseUrl(paymentProperties.apiBaseUrl)
|
.baseUrl(paymentProperties.apiBaseUrl)
|
||||||
|
.observationRegistry(observationRegistry)
|
||||||
.defaultHeader("Authorization", getAuthorizations(paymentProperties.confirmSecretKey))
|
.defaultHeader("Authorization", getAuthorizations(paymentProperties.confirmSecretKey))
|
||||||
.requestFactory(requestFactory)
|
.requestFactory(requestFactory)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,3 +7,4 @@ include 'common:types'
|
|||||||
include 'common:log'
|
include 'common:log'
|
||||||
include 'common:persistence'
|
include 'common:persistence'
|
||||||
include 'common:utils'
|
include 'common:utils'
|
||||||
|
include 'tosspay-mock'
|
||||||
3
tosspay-mock/.gitattributes
vendored
Normal file
3
tosspay-mock/.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
/gradlew text eol=lf
|
||||||
|
*.bat text eol=crlf
|
||||||
|
*.jar binary
|
||||||
40
tosspay-mock/.gitignore
vendored
Normal file
40
tosspay-mock/.gitignore
vendored
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
HELP.md
|
||||||
|
.gradle
|
||||||
|
build/
|
||||||
|
!gradle/wrapper/gradle-wrapper.jar
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### 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/
|
||||||
|
|
||||||
|
### Kotlin ###
|
||||||
|
.kotlin
|
||||||
28
tosspay-mock/build.gradle.kts
Normal file
28
tosspay-mock/build.gradle.kts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
plugins {
|
||||||
|
id("org.springframework.boot")
|
||||||
|
kotlin("plugin.spring")
|
||||||
|
kotlin("plugin.jpa")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation("org.springframework.boot:spring-boot-starter-actuator")
|
||||||
|
implementation("net.logstash.logback:logstash-logback-encoder:8.1")
|
||||||
|
implementation("io.micrometer:micrometer-tracing-bridge-otel")
|
||||||
|
implementation("io.opentelemetry:opentelemetry-exporter-otlp")
|
||||||
|
|
||||||
|
implementation(project(":common:web"))
|
||||||
|
implementation(project(":common:types"))
|
||||||
|
implementation(project(":common:persistence"))
|
||||||
|
|
||||||
|
runtimeOnly("io.micrometer:micrometer-registry-prometheus")
|
||||||
|
runtimeOnly("com.h2database:h2")
|
||||||
|
|
||||||
|
testImplementation("org.springframework.boot:spring-boot-starter-test")
|
||||||
|
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.3.0")
|
||||||
|
testImplementation("io.rest-assured:rest-assured:5.5.5")
|
||||||
|
testImplementation("io.rest-assured:kotlin-extensions:5.5.5")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named<Jar>("jar") {
|
||||||
|
enabled = false
|
||||||
|
}
|
||||||
BIN
tosspay-mock/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
tosspay-mock/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
tosspay-mock/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
tosspay-mock/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
251
tosspay-mock/gradlew
vendored
Executable file
251
tosspay-mock/gradlew
vendored
Executable file
@ -0,0 +1,251 @@
|
|||||||
|
#!/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.
|
||||||
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
|
##############################################################################
|
||||||
|
#
|
||||||
|
# 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/platforms/jvm/plugins-application/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 -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || 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="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
|
# 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, 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" \
|
||||||
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
|
"$@"
|
||||||
|
|
||||||
|
# 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" "$@"
|
||||||
94
tosspay-mock/gradlew.bat
vendored
Normal file
94
tosspay-mock/gradlew.bat
vendored
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
@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
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@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. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:findJavaFromJavaHome
|
||||||
|
set JAVA_HOME=%JAVA_HOME:"=%
|
||||||
|
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||||
|
|
||||||
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
|
echo. 1>&2
|
||||||
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
|
echo. 1>&2
|
||||||
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
|
goto fail
|
||||||
|
|
||||||
|
:execute
|
||||||
|
@rem Setup the command line
|
||||||
|
|
||||||
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
|
@rem Execute Gradle
|
||||||
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
|
: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
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
package com.sangdol.tosspaymock
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication
|
||||||
|
import org.springframework.boot.runApplication
|
||||||
|
|
||||||
|
@SpringBootApplication(
|
||||||
|
scanBasePackages = ["com.sangdol.tosspaymock", "com.sangdol.common"]
|
||||||
|
)
|
||||||
|
class TosspayMockApplication
|
||||||
|
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
runApplication<TosspayMockApplication>(*args)
|
||||||
|
}
|
||||||
@ -0,0 +1,111 @@
|
|||||||
|
package com.sangdol.tosspaymock.business
|
||||||
|
|
||||||
|
import com.sangdol.common.persistence.IDGenerator
|
||||||
|
import com.sangdol.tosspaymock.business.domain.Payment
|
||||||
|
import com.sangdol.tosspaymock.business.domain.cancel.Cancellation
|
||||||
|
import com.sangdol.tosspaymock.exception.TosspayException
|
||||||
|
import com.sangdol.tosspaymock.exception.code.TosspayCancelErrorCode
|
||||||
|
import com.sangdol.tosspaymock.infrastructure.persistence.OrderAmountEntity
|
||||||
|
import com.sangdol.tosspaymock.infrastructure.persistence.OrderAmountRepository
|
||||||
|
import com.sangdol.tosspaymock.web.dto.PaymentCancelRequest
|
||||||
|
import com.sangdol.tosspaymock.web.dto.PaymentConfirmRequest
|
||||||
|
import com.sangdol.tosspaymock.web.dto.PaymentResponse
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class TosspayService(
|
||||||
|
private val idGenerator: IDGenerator,
|
||||||
|
private val orderAmountRepository: OrderAmountRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun confirm(request: PaymentConfirmRequest): PaymentResponse {
|
||||||
|
log.info { "[TosspayService.confirm] 결제 확정 시작: paymentKey=${request.paymentKey}, amount=${request.amount}" }
|
||||||
|
|
||||||
|
val payment = choosePayment(request).also { saveAmount(request.paymentKey, it) }
|
||||||
|
|
||||||
|
return payment.toResponse()
|
||||||
|
.also {
|
||||||
|
log.info { "[TosspayService.confirm] 결제 확정 완료: paymentKey=${request.paymentKey}, amount=${request.amount}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancel(paymentKey: String, request: PaymentCancelRequest): PaymentResponse {
|
||||||
|
log.info { "[TosspayService.cancel] 결제 취소 시작: paymentKey=${paymentKey}" }
|
||||||
|
|
||||||
|
val orderAmount = orderAmountRepository.findByPaymentKey(paymentKey)
|
||||||
|
?: throw TosspayException(TosspayCancelErrorCode.NOT_FOUND_PAYMENT)
|
||||||
|
|
||||||
|
val cancellation = Cancellation.random(
|
||||||
|
cancelReason = request.cancelReason,
|
||||||
|
cancelAmount = request.cancelAmount ?: orderAmount.totalAmount(),
|
||||||
|
easyPayDiscountAmount = orderAmount.easypayDiscountAmount,
|
||||||
|
cardDiscountAmount = orderAmount.cardDiscountAmount,
|
||||||
|
transferDiscountAmount = orderAmount.transferDiscountAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
return Payment.randomForCancellation(paymentKey, cancellation)
|
||||||
|
.toResponse()
|
||||||
|
.also {
|
||||||
|
log.info { "[TosspayService.cancel] 결제 취소 완료: paymentKey=${paymentKey}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun choosePayment(request: PaymentConfirmRequest): Payment {
|
||||||
|
log.info { "[TosspayService.choosePayment] 랜덤 결제 정보 생성 시작: paymentKey=${request.paymentKey}, amount=${request.amount}" }
|
||||||
|
val randomValue = Math.random()
|
||||||
|
|
||||||
|
// 70%는 간편결제에 배정
|
||||||
|
return if (randomValue < 0.7) {
|
||||||
|
// 70%의 간편결제 중 70%는 카드
|
||||||
|
if (randomValue < 0.49) {
|
||||||
|
Payment.randomWithEasypayCard(
|
||||||
|
request.paymentKey, request.orderId, request.amount, request.requestedAt
|
||||||
|
).also {
|
||||||
|
log.info { "[Tosspayment.choosePayment] 간편결제 + 카드 결제 객체 생성 완료" }
|
||||||
|
}
|
||||||
|
} else { // 30%는 간편결제 선불 충전액
|
||||||
|
Payment.randomWithEasypayPrepaid(
|
||||||
|
request.paymentKey, request.orderId, request.amount, request.requestedAt
|
||||||
|
).also {
|
||||||
|
log.info { "[Tosspayment.choosePayment] 간편결제 + 충전식 결제 객체 생성 완료" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (randomValue < 0.95) { // 남은 30% 중 25%는 일반 카드
|
||||||
|
Payment.randomWithCard(
|
||||||
|
request.paymentKey, request.orderId, request.amount, request.requestedAt
|
||||||
|
).also {
|
||||||
|
log.info { "[Tosspayment.choosePayment] 카드 결제 객체 생성 완료" }
|
||||||
|
}
|
||||||
|
} else { // 나머지는 계좌이체
|
||||||
|
Payment.randomWithBankTransfer(
|
||||||
|
request.paymentKey, request.orderId, request.amount, request.requestedAt
|
||||||
|
).also {
|
||||||
|
log.info { "[Tosspayment.choosePayment] 계좌이체 결제 객체 생성 완료" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveAmount(paymentKey: String, payment: Payment) {
|
||||||
|
log.info { "[Tosspayment.saveAmount] 결제 금액 정보 저장 시작: paymentKey=${paymentKey}" }
|
||||||
|
val easypayDiscountAmount = payment.easyPay?.discountAmount ?: 0
|
||||||
|
val cardDiscountAmount = 0
|
||||||
|
val transferDiscountAmount = 0
|
||||||
|
|
||||||
|
val orderAmount = OrderAmountEntity(
|
||||||
|
idGenerator.create(),
|
||||||
|
paymentKey,
|
||||||
|
(payment.totalAmount - easypayDiscountAmount),
|
||||||
|
easypayDiscountAmount,
|
||||||
|
cardDiscountAmount,
|
||||||
|
transferDiscountAmount
|
||||||
|
)
|
||||||
|
|
||||||
|
orderAmountRepository.save(orderAmount).also {
|
||||||
|
log.info { "[Tosspayment.saveAmount] 결제 금액 정보 저장 완료: id=${it.id}, paymentKey=${paymentKey}, amount=${orderAmount.approvedAmount}, easypayDiscount=${orderAmount.easypayDiscountAmount}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,221 @@
|
|||||||
|
package com.sangdol.tosspaymock.business.domain
|
||||||
|
|
||||||
|
import com.sangdol.tosspaymock.business.domain.cancel.Cancellation
|
||||||
|
import com.sangdol.tosspaymock.business.domain.card.Card
|
||||||
|
import com.sangdol.tosspaymock.business.domain.easypay.Easypay
|
||||||
|
import com.sangdol.tosspaymock.business.domain.transfer.BankTransfer
|
||||||
|
import com.sangdol.tosspaymock.web.dto.PaymentResponse
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
class Payment(
|
||||||
|
val mid: String,
|
||||||
|
val lastTransactionKey: String,
|
||||||
|
val paymentKey: String,
|
||||||
|
val orderId: String,
|
||||||
|
val orderName: String,
|
||||||
|
val taxExemptionAmount: Int = 0,
|
||||||
|
var status: PaymentStatus = PaymentStatus.DONE,
|
||||||
|
val requestedAt: OffsetDateTime,
|
||||||
|
val approvedAt: OffsetDateTime,
|
||||||
|
val useEscrow: Boolean = false,
|
||||||
|
val cultureExpense: Boolean = false,
|
||||||
|
val card: Card? = null,
|
||||||
|
val virtualAccount: String? = null,
|
||||||
|
val transfer: BankTransfer? = null,
|
||||||
|
val mobilePhone: String? = null,
|
||||||
|
val giftCertificate: String? = null,
|
||||||
|
val cashReceipt: String? = null,
|
||||||
|
val cashReceipts: String? = null,
|
||||||
|
val discount: String? = null,
|
||||||
|
var cancels: Cancellation? = null,
|
||||||
|
val secret: String? = null,
|
||||||
|
val type: PaymentType = PaymentType.NORMAL,
|
||||||
|
val easyPay: Easypay? = null,
|
||||||
|
val country: String = "KR",
|
||||||
|
val failure: String? = null,
|
||||||
|
val isPartialCancelable: Boolean = true,
|
||||||
|
val receipt: String? = null,
|
||||||
|
val checkout: String? = null,
|
||||||
|
val currency: String = "KRW",
|
||||||
|
val totalAmount: Int,
|
||||||
|
val balanceAmount: Int,
|
||||||
|
val suppliedAmount: Int,
|
||||||
|
val vat: Int,
|
||||||
|
val taxFreeAmount: Int = 0,
|
||||||
|
val method: PaymentMethod,
|
||||||
|
val version: String = "2022-11-16",
|
||||||
|
val metadata: String? = null
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun randomWithCard(
|
||||||
|
paymentKey: String,
|
||||||
|
orderId: String,
|
||||||
|
amount: Int,
|
||||||
|
requestedAt: OffsetDateTime
|
||||||
|
): Payment {
|
||||||
|
val card = Card.random(amount)
|
||||||
|
|
||||||
|
return create(
|
||||||
|
paymentKey = paymentKey,
|
||||||
|
orderId = orderId,
|
||||||
|
requestedAt = requestedAt,
|
||||||
|
amount = amount,
|
||||||
|
card = card,
|
||||||
|
easyPay = null,
|
||||||
|
bankTransfer = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomWithEasypayCard(
|
||||||
|
paymentKey: String,
|
||||||
|
orderId: String,
|
||||||
|
amount: Int,
|
||||||
|
requestedAt: OffsetDateTime
|
||||||
|
): Payment {
|
||||||
|
val easyPay = Easypay.randomWithCard(amount)
|
||||||
|
val card = Card.random(approvedAmount = (amount - easyPay.discountAmount))
|
||||||
|
|
||||||
|
return create(
|
||||||
|
paymentKey = paymentKey,
|
||||||
|
orderId = orderId,
|
||||||
|
requestedAt = requestedAt,
|
||||||
|
amount = amount,
|
||||||
|
card = card,
|
||||||
|
easyPay = easyPay,
|
||||||
|
bankTransfer = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomWithEasypayPrepaid(
|
||||||
|
paymentKey: String,
|
||||||
|
orderId: String,
|
||||||
|
amount: Int,
|
||||||
|
requestedAt: OffsetDateTime
|
||||||
|
): Payment {
|
||||||
|
val easypay = Easypay.randomWithPrepaid(amount)
|
||||||
|
|
||||||
|
return create(
|
||||||
|
paymentKey = paymentKey,
|
||||||
|
orderId = orderId,
|
||||||
|
requestedAt = requestedAt,
|
||||||
|
amount = amount,
|
||||||
|
card = null,
|
||||||
|
easyPay = easypay,
|
||||||
|
bankTransfer = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomWithBankTransfer(
|
||||||
|
paymentKey: String,
|
||||||
|
orderId: String,
|
||||||
|
amount: Int,
|
||||||
|
requestedAt: OffsetDateTime
|
||||||
|
): Payment {
|
||||||
|
val bankTransfer = BankTransfer.random()
|
||||||
|
|
||||||
|
return create(
|
||||||
|
paymentKey = paymentKey,
|
||||||
|
orderId = orderId,
|
||||||
|
requestedAt = requestedAt,
|
||||||
|
amount = amount,
|
||||||
|
card = null,
|
||||||
|
easyPay = null,
|
||||||
|
bankTransfer = bankTransfer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomForCancellation(paymentKey: String, cancellation: Cancellation): Payment {
|
||||||
|
return create(
|
||||||
|
paymentKey = paymentKey,
|
||||||
|
orderId = "orderId",
|
||||||
|
requestedAt = OffsetDateTime.now(),
|
||||||
|
amount = cancellation.cancelAmount,
|
||||||
|
card = null,
|
||||||
|
easyPay = null,
|
||||||
|
bankTransfer = null
|
||||||
|
).apply {
|
||||||
|
this.status = PaymentStatus.CANCELED
|
||||||
|
this.cancels = cancellation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun create(
|
||||||
|
paymentKey: String,
|
||||||
|
orderId: String,
|
||||||
|
requestedAt: OffsetDateTime,
|
||||||
|
amount: Int,
|
||||||
|
card: Card?,
|
||||||
|
easyPay: Easypay?,
|
||||||
|
bankTransfer: BankTransfer?
|
||||||
|
): Payment {
|
||||||
|
val vat = (amount * 0.1).toInt()
|
||||||
|
val suppliedAmount = amount - vat
|
||||||
|
val paymentMethod = if (bankTransfer != null) {
|
||||||
|
PaymentMethod.TRANSFER
|
||||||
|
} else if (easyPay != null) {
|
||||||
|
PaymentMethod.EASY_PAY
|
||||||
|
} else {
|
||||||
|
PaymentMethod.CARD
|
||||||
|
}
|
||||||
|
|
||||||
|
return Payment(
|
||||||
|
mid = RandomPaymentValueGenerator.mId(),
|
||||||
|
lastTransactionKey = RandomPaymentValueGenerator.transactionKey(),
|
||||||
|
paymentKey = paymentKey,
|
||||||
|
orderId = orderId,
|
||||||
|
orderName = "테스트 결제",
|
||||||
|
requestedAt = requestedAt,
|
||||||
|
approvedAt = OffsetDateTime.now(),
|
||||||
|
card = card,
|
||||||
|
easyPay = easyPay,
|
||||||
|
transfer = bankTransfer,
|
||||||
|
totalAmount = amount,
|
||||||
|
balanceAmount = amount,
|
||||||
|
suppliedAmount = suppliedAmount,
|
||||||
|
vat = vat,
|
||||||
|
method = paymentMethod,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun toResponse() = PaymentResponse(
|
||||||
|
mid = this.mid,
|
||||||
|
lastTransactionKey = this.lastTransactionKey,
|
||||||
|
paymentKey = this.paymentKey,
|
||||||
|
orderId = this.orderId,
|
||||||
|
orderName = this.orderName,
|
||||||
|
taxExemptionAmount = this.taxExemptionAmount,
|
||||||
|
status = this.status.name,
|
||||||
|
requestedAt = this.requestedAt,
|
||||||
|
approvedAt = this.approvedAt,
|
||||||
|
useEscrow = this.useEscrow,
|
||||||
|
cultureExpense = this.cultureExpense,
|
||||||
|
card = this.card?.toResponse(),
|
||||||
|
virtualAccount = this.virtualAccount,
|
||||||
|
transfer = this.transfer?.toResponse(),
|
||||||
|
mobilePhone = this.mobilePhone,
|
||||||
|
giftCertificate = this.giftCertificate,
|
||||||
|
cashReceipt = this.cashReceipt,
|
||||||
|
cashReceipts = this.cashReceipts,
|
||||||
|
discount = this.discount,
|
||||||
|
cancels = this.cancels?.toResponse(),
|
||||||
|
secret = this.secret,
|
||||||
|
type = this.type,
|
||||||
|
easyPay = this.easyPay?.toResponse(),
|
||||||
|
country = this.country,
|
||||||
|
failure = this.failure,
|
||||||
|
isPartialCancelable = this.isPartialCancelable,
|
||||||
|
receipt = this.receipt,
|
||||||
|
checkout = this.checkout,
|
||||||
|
currency = this.currency,
|
||||||
|
totalAmount = this.totalAmount,
|
||||||
|
balanceAmount = this.balanceAmount,
|
||||||
|
suppliedAmount = this.suppliedAmount,
|
||||||
|
vat = this.vat,
|
||||||
|
taxFreeAmount = this.taxFreeAmount,
|
||||||
|
method = this.method.koreanName,
|
||||||
|
version = this.version,
|
||||||
|
metadata = this.metadata,
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
package com.sangdol.tosspaymock.business.domain
|
||||||
|
|
||||||
|
enum class PaymentType(
|
||||||
|
) {
|
||||||
|
NORMAL,
|
||||||
|
BILLING,
|
||||||
|
BRANDPAY,
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PaymentMethod(
|
||||||
|
val koreanName: String,
|
||||||
|
) {
|
||||||
|
CARD("카드"),
|
||||||
|
EASY_PAY("간편결제"),
|
||||||
|
VIRTUAL_ACCOUNT("가상계좌"),
|
||||||
|
MOBILE_PHONE("휴대폰"),
|
||||||
|
TRANSFER("계좌이체"),
|
||||||
|
CULTURE_GIFT_CERTIFICATE("문화상품권"),
|
||||||
|
BOOK_GIFT_CERTIFICATE("도서문화상품권"),
|
||||||
|
GAME_GIFT_CERTIFICATE("게임문화상품권"),
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class PaymentStatus {
|
||||||
|
IN_PROGRESS,
|
||||||
|
DONE,
|
||||||
|
CANCELED,
|
||||||
|
ABORTED,
|
||||||
|
EXPIRED,
|
||||||
|
;
|
||||||
|
}
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
package com.sangdol.tosspaymock.business.domain
|
||||||
|
|
||||||
|
object RandomCardValueGenerator {
|
||||||
|
fun cardNumber(): String {
|
||||||
|
return "${(10000000..99999999).random()}****${(100..999).random()}*"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun approvalNumber(): String {
|
||||||
|
return "${(0..99999999).random()}".padStart(8, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
fun installmentPlanMonths(amount: Int): Int {
|
||||||
|
return if (amount < 50_000 || Math.random() < 0.95) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
(1..6).random()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object RandomEasypayValueGenerator {
|
||||||
|
fun point(amount: Int): Int =
|
||||||
|
if (amount < 100 || Math.random() < 0.8) {
|
||||||
|
0
|
||||||
|
} else {
|
||||||
|
// 100~amount 까지 100원 단위로 생성
|
||||||
|
((100..amount).random() / 100) * 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object RandomPaymentValueGenerator {
|
||||||
|
fun mId(): String {
|
||||||
|
val words = ('a'..'z')
|
||||||
|
val randomValue = (1..9).map { words.random() }.joinToString("")
|
||||||
|
|
||||||
|
return "tgen_${randomValue}"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun transactionKey(): String {
|
||||||
|
val prefix = "txrd"
|
||||||
|
val characters = ('0'..'9') + ('a'..'z')
|
||||||
|
val randomString = (1..24).map { characters.random() }.joinToString("")
|
||||||
|
return "${prefix}_${randomString}"
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
package com.sangdol.tosspaymock.business.domain.cancel
|
||||||
|
|
||||||
|
import com.sangdol.tosspaymock.business.domain.RandomPaymentValueGenerator
|
||||||
|
import com.sangdol.tosspaymock.web.dto.CancelResponse
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
class Cancellation(
|
||||||
|
val transactionKey: String,
|
||||||
|
val cancelReason: String,
|
||||||
|
val taxExemptionAmount: Int = 0,
|
||||||
|
val canceledAt: OffsetDateTime,
|
||||||
|
val cardDiscountAmount: Int,
|
||||||
|
val transferDiscountAmount: Int,
|
||||||
|
val easyPayDiscountAmount: Int,
|
||||||
|
val receiptKey: String? = null,
|
||||||
|
val cancelStatus: String = "DONE",
|
||||||
|
val cancelRequestId: String? = null,
|
||||||
|
val cancelAmount: Int,
|
||||||
|
val taxFreeAmount: Int = 0,
|
||||||
|
val refundableAmount: Int = 0,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun random(
|
||||||
|
cancelReason: String,
|
||||||
|
cancelAmount: Int,
|
||||||
|
easyPayDiscountAmount: Int,
|
||||||
|
cardDiscountAmount: Int,
|
||||||
|
transferDiscountAmount: Int,
|
||||||
|
) = Cancellation(
|
||||||
|
transactionKey = RandomPaymentValueGenerator.transactionKey(),
|
||||||
|
cancelReason = cancelReason,
|
||||||
|
canceledAt = OffsetDateTime.now(),
|
||||||
|
cardDiscountAmount = cardDiscountAmount,
|
||||||
|
transferDiscountAmount = transferDiscountAmount,
|
||||||
|
easyPayDiscountAmount = easyPayDiscountAmount,
|
||||||
|
cancelAmount = cancelAmount,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toResponse() = CancelResponse(
|
||||||
|
transactionKey = this.transactionKey,
|
||||||
|
cancelReason = this.cancelReason,
|
||||||
|
taxExemptionAmount = this.taxExemptionAmount,
|
||||||
|
canceledAt = this.canceledAt,
|
||||||
|
cardDiscountAmount = this.cardDiscountAmount,
|
||||||
|
transferDiscountAmount = this.transferDiscountAmount,
|
||||||
|
easyPayDiscountAmount = this.easyPayDiscountAmount,
|
||||||
|
receiptKey = this.receiptKey,
|
||||||
|
cancelStatus = this.cancelStatus,
|
||||||
|
cancelRequestId = this.cancelRequestId,
|
||||||
|
cancelAmount = this.cancelAmount,
|
||||||
|
taxFreeAmount = this.taxFreeAmount,
|
||||||
|
refundableAmount = this.refundableAmount
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
package com.sangdol.tosspaymock.business.domain.card
|
||||||
|
|
||||||
|
import com.sangdol.tosspaymock.business.domain.RandomCardValueGenerator
|
||||||
|
import com.sangdol.tosspaymock.web.dto.CardResponse
|
||||||
|
|
||||||
|
class Card(
|
||||||
|
val issuerCode: CardIssuerCode,
|
||||||
|
val acquirerCode: CardIssuerCode,
|
||||||
|
val number: String,
|
||||||
|
val installmentPlanMonths: Int,
|
||||||
|
val isInterestFree: Boolean = true,
|
||||||
|
val interestPayer: String? = null,
|
||||||
|
val approveNo: String,
|
||||||
|
val useCardPoint: Boolean,
|
||||||
|
val cardType: CardType,
|
||||||
|
val ownerType: CardOwnerType,
|
||||||
|
val acquireStatus: CardAcquireStatus,
|
||||||
|
val amount: Int,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
val availableCardTypes = listOf(CardType.CREDIT, CardType.CHECK)
|
||||||
|
val availableCardOwnerTypes = CardOwnerType.entries - CardOwnerType.UNKNOWN
|
||||||
|
fun random(approvedAmount: Int): Card {
|
||||||
|
return Card(
|
||||||
|
issuerCode = CardIssuerCode.entries.random(),
|
||||||
|
acquirerCode = CardIssuerCode.entries.random(),
|
||||||
|
number = RandomCardValueGenerator.cardNumber(),
|
||||||
|
installmentPlanMonths = RandomCardValueGenerator.installmentPlanMonths(approvedAmount),
|
||||||
|
isInterestFree = true,
|
||||||
|
interestPayer = null,
|
||||||
|
approveNo = RandomCardValueGenerator.approvalNumber(),
|
||||||
|
useCardPoint = false,
|
||||||
|
cardType = availableCardTypes.random(),
|
||||||
|
ownerType = availableCardOwnerTypes.random(),
|
||||||
|
acquireStatus = CardAcquireStatus.COMPLETED,
|
||||||
|
amount = approvedAmount
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun toResponse() = CardResponse(
|
||||||
|
issuerCode = this.issuerCode.code,
|
||||||
|
acquirerCode = this.acquirerCode.code,
|
||||||
|
number = this.number,
|
||||||
|
installmentPlanMonths = this.installmentPlanMonths,
|
||||||
|
isInterestFree = this.isInterestFree,
|
||||||
|
interestPayer = this.interestPayer,
|
||||||
|
approveNo = this.approveNo,
|
||||||
|
useCardPoint = this.useCardPoint,
|
||||||
|
cardType = this.cardType.koreanName,
|
||||||
|
ownerType = this.ownerType.koreanName,
|
||||||
|
acquireStatus = this.acquireStatus.name,
|
||||||
|
amount = this.amount
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,58 @@
|
|||||||
|
package com.sangdol.tosspaymock.business.domain.card
|
||||||
|
|
||||||
|
enum class CardType(
|
||||||
|
val koreanName: String
|
||||||
|
) {
|
||||||
|
CREDIT("신용"),
|
||||||
|
CHECK("체크"),
|
||||||
|
GIFT("기프트"),
|
||||||
|
UNKNOWN("미확인"),
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CardOwnerType(
|
||||||
|
val koreanName: String
|
||||||
|
) {
|
||||||
|
PERSONAL("개인"),
|
||||||
|
CORPORATE("법인"),
|
||||||
|
UNKNOWN("미확인"),
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CardAcquireStatus {
|
||||||
|
READY,
|
||||||
|
REQUESTED,
|
||||||
|
COMPLETED,
|
||||||
|
CANCEL_REQUESTED,
|
||||||
|
CANCELED
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class CardIssuerCode(
|
||||||
|
val code: String,
|
||||||
|
) {
|
||||||
|
IBK_BC("3K"),
|
||||||
|
GWANGJU_BANK("46"),
|
||||||
|
LOTTE("71"),
|
||||||
|
KDB_BANK("30"),
|
||||||
|
BC("31"),
|
||||||
|
SAMSUNG("51"),
|
||||||
|
SAEMAUL("38"),
|
||||||
|
SHINHAN("41"),
|
||||||
|
SHINHYEOP("62"),
|
||||||
|
CITI("36"),
|
||||||
|
WOORI_BC("33"),
|
||||||
|
WOORI("W1"),
|
||||||
|
POST("37"),
|
||||||
|
SAVINGBANK("39"),
|
||||||
|
JEONBUK_BANK("35"),
|
||||||
|
JEJU_BANK("42"),
|
||||||
|
KAKAO_BANK("15"),
|
||||||
|
K_BANK("3A"),
|
||||||
|
TOSS_BANK("24"),
|
||||||
|
HANA("21"),
|
||||||
|
HYUNDAI("61"),
|
||||||
|
KOOKMIN("11"),
|
||||||
|
NONGHYEOP("91"),
|
||||||
|
SUHYEOP("34"),
|
||||||
|
;
|
||||||
|
}
|
||||||
@ -0,0 +1,38 @@
|
|||||||
|
package com.sangdol.tosspaymock.business.domain.easypay
|
||||||
|
|
||||||
|
import com.sangdol.tosspaymock.business.domain.RandomEasypayValueGenerator
|
||||||
|
import com.sangdol.tosspaymock.web.dto.EasypayResponse
|
||||||
|
|
||||||
|
class Easypay(
|
||||||
|
val provider: EasypayProvider,
|
||||||
|
val amount: Int,
|
||||||
|
val discountAmount: Int
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun randomWithPrepaid(amount: Int): Easypay {
|
||||||
|
val point = RandomEasypayValueGenerator.point(amount)
|
||||||
|
|
||||||
|
return Easypay(
|
||||||
|
provider = EasypayProvider.entries.random(),
|
||||||
|
amount = (amount - point),
|
||||||
|
discountAmount = point
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun randomWithCard(amount: Int): Easypay {
|
||||||
|
val point = RandomEasypayValueGenerator.point(amount)
|
||||||
|
|
||||||
|
return Easypay(
|
||||||
|
provider = EasypayProvider.entries.random(),
|
||||||
|
amount = 0,
|
||||||
|
discountAmount = point
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toResponse() = EasypayResponse(
|
||||||
|
provider = this.provider.koreanName,
|
||||||
|
amount = this.amount,
|
||||||
|
discountAmount = this.discountAmount
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
package com.sangdol.tosspaymock.business.domain.easypay
|
||||||
|
|
||||||
|
enum class EasypayProvider(
|
||||||
|
val koreanName: String
|
||||||
|
) {
|
||||||
|
TOSSPAY("토스페이"),
|
||||||
|
NAVERPAY("네이버페이"),
|
||||||
|
SAMSUNGPAY("삼성페이"),
|
||||||
|
LPAY("엘페이"),
|
||||||
|
KAKAOPAY("카카오페이"),
|
||||||
|
PAYCO("페이코"),
|
||||||
|
SSG("SSG페이"),
|
||||||
|
APPLEPAY("애플페이"),
|
||||||
|
PINPAY("핀페이"),
|
||||||
|
;
|
||||||
|
}
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
package com.sangdol.tosspaymock.business.domain.transfer
|
||||||
|
|
||||||
|
import com.sangdol.tosspaymock.web.dto.BankTransferResponse
|
||||||
|
|
||||||
|
class BankTransfer(
|
||||||
|
val bankCode: BankCode,
|
||||||
|
val settlementStatus: SettlementStatus,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun random(): BankTransfer {
|
||||||
|
return BankTransfer(
|
||||||
|
bankCode = BankCode.entries.random(),
|
||||||
|
settlementStatus = SettlementStatus.COMPLETED
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toResponse() = BankTransferResponse(
|
||||||
|
bankCode = this.bankCode.code,
|
||||||
|
settlementStatus = this.settlementStatus.name
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
package com.sangdol.tosspaymock.business.domain.transfer
|
||||||
|
|
||||||
|
enum class BankCode(
|
||||||
|
val code: String,
|
||||||
|
) {
|
||||||
|
KYONGNAM_BANK("039"),
|
||||||
|
GWANGJU_BANK("034"),
|
||||||
|
LOCAL_NONGHYEOP("012"),
|
||||||
|
BUSAN_BANK("032"),
|
||||||
|
SAEMAUL("045"),
|
||||||
|
SANLIM("064"),
|
||||||
|
SHINHAN("088"),
|
||||||
|
SHINHYEOP("048"),
|
||||||
|
CITI("027"),
|
||||||
|
WOORI("020"),
|
||||||
|
POST("071"),
|
||||||
|
SAVINGBANK("050"),
|
||||||
|
JEONBUK_BANK("037"),
|
||||||
|
JEJU_BANK("035"),
|
||||||
|
KAKAO_BANK("090"),
|
||||||
|
K_BANK("089"),
|
||||||
|
TOSS_BANK("092"),
|
||||||
|
HANA("081"),
|
||||||
|
HSBC("054"),
|
||||||
|
IBK("003"),
|
||||||
|
KOOKMIN("004"),
|
||||||
|
DAEGU("031"),
|
||||||
|
KDB_BANK("002"),
|
||||||
|
NONGHYEOP("011"),
|
||||||
|
SC("023"),
|
||||||
|
SUHYEOP("007");
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class SettlementStatus {
|
||||||
|
COMPLETED,
|
||||||
|
INCOMPLETED,
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.sangdol.tosspaymock.exception
|
||||||
|
|
||||||
|
import com.sangdol.tosspaymock.exception.code.TosspayErrorCode
|
||||||
|
|
||||||
|
class TosspayException(
|
||||||
|
val errorCode: TosspayErrorCode,
|
||||||
|
override val message: String = errorCode.message
|
||||||
|
) : RuntimeException(message)
|
||||||
@ -0,0 +1,29 @@
|
|||||||
|
package com.sangdol.tosspaymock.exception
|
||||||
|
|
||||||
|
import com.sangdol.common.types.web.CommonErrorResponse
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
@RestControllerAdvice
|
||||||
|
class TosspayExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(value = [TosspayException::class])
|
||||||
|
fun handleTosspayException(
|
||||||
|
e: TosspayException
|
||||||
|
): ResponseEntity<CommonErrorResponse> {
|
||||||
|
val code = e.errorCode
|
||||||
|
val name = (code as Enum<*>).name
|
||||||
|
val message = e.message
|
||||||
|
|
||||||
|
log.warn { "[TosspayExceptionHandler] 결제 처리 과정 중 오류 발생: error=${name}, message=$message" }
|
||||||
|
|
||||||
|
return ResponseEntity
|
||||||
|
.status(code.httpStatus.value())
|
||||||
|
.body(CommonErrorResponse(name, message))
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.sangdol.tosspaymock.exception.code
|
||||||
|
|
||||||
|
import com.sangdol.common.types.web.HttpStatus
|
||||||
|
|
||||||
|
enum class TosspayCancelErrorCode(
|
||||||
|
override val httpStatus: HttpStatus,
|
||||||
|
override val errorCode: String,
|
||||||
|
override val message: String
|
||||||
|
) : TosspayErrorCode {
|
||||||
|
ALREADY_CANCELED_PAYMENT(HttpStatus.BAD_REQUEST, "CO001", "이미 취소된 결제 입니다."),
|
||||||
|
INVALID_REFUND_ACCOUNT_INFO(HttpStatus.BAD_REQUEST, "CO002", "환불 계좌번호와 예금주명이 일치하지 않습니다."),
|
||||||
|
EXCEED_CANCEL_AMOUNT_DISCOUNT_AMOUNT(HttpStatus.BAD_REQUEST, "CO003", "즉시할인금액보다 적은 금액은 부분취소가 불가능합니다."),
|
||||||
|
INVALID_REQUEST(HttpStatus.BAD_REQUEST, "CO004", "잘못된 요청입니다."),
|
||||||
|
INVALID_REFUND_ACCOUNT_NUMBER(HttpStatus.BAD_REQUEST, "CO005", "잘못된 환불 계좌번호입니다."),
|
||||||
|
INVALID_BANK(HttpStatus.BAD_REQUEST, "CO006", "유효하지 않은 은행입니다."),
|
||||||
|
NOT_MATCHES_REFUNDABLE_AMOUNT(HttpStatus.BAD_REQUEST, "CO007", "잔액 결과가 일치하지 않습니다."),
|
||||||
|
PROVIDER_ERROR(HttpStatus.BAD_REQUEST, "CO008", "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."),
|
||||||
|
REFUND_REJECTED(HttpStatus.BAD_REQUEST, "CO009", "환불이 거절됐습니다. 결제사에 문의 부탁드립니다."),
|
||||||
|
ALREADY_REFUND_PAYMENT(HttpStatus.BAD_REQUEST, "CO010", "이미 환불된 결제입니다."),
|
||||||
|
FORBIDDEN_BANK_REFUND_REQUEST(HttpStatus.BAD_REQUEST, "CO011", "고객 계좌가 입금이 되지 않는 상태입니다."),
|
||||||
|
UNAUTHORIZED_KEY(HttpStatus.UNAUTHORIZED, "CO012", "인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다."),
|
||||||
|
NOT_CANCELABLE_AMOUNT(HttpStatus.FORBIDDEN, "CO013", "취소 할 수 없는 금액 입니다."),
|
||||||
|
FORBIDDEN_CONSECUTIVE_REQUEST(HttpStatus.FORBIDDEN, "CO014", "반복적인 요청은 허용되지 않습니다. 잠시 후 다시 시도해주세요."),
|
||||||
|
FORBIDDEN_REQUEST(HttpStatus.FORBIDDEN, "CO015", "허용되지 않은 요청입니다."),
|
||||||
|
NOT_CANCELABLE_PAYMENT(HttpStatus.FORBIDDEN, "CO016", "취소 할 수 없는 결제 입니다."),
|
||||||
|
EXCEED_MAX_REFUND_DUE(HttpStatus.FORBIDDEN, "CO017", "환불 가능한 기간이 지났습니다."),
|
||||||
|
NOT_ALLOWED_PARTIAL_REFUND_WAITING_DEPOSIT(HttpStatus.FORBIDDEN, "CO018", "입금 대기중인 결제는 부분 환불이 불가합니다."),
|
||||||
|
NOT_ALLOWED_PARTIAL_REFUND(HttpStatus.FORBIDDEN, "CO019", "에스크로 주문, 현금 카드 결제일 때는 부분 환불이 불가합니다. 이외 다른 결제 수단에서 부분 취소가 되지 않을 때는 토스페이먼츠에 문의해 주세요."),
|
||||||
|
NOT_AVAILABLE_BANK(HttpStatus.FORBIDDEN, "CO020", "은행 서비스 시간이 아닙니다."),
|
||||||
|
INCORRECT_BASIC_AUTH_FORMAT(HttpStatus.FORBIDDEN, "CO021", "잘못된 요청입니다. ':' 를 포함해 인코딩해주세요."),
|
||||||
|
NOT_CANCELABLE_PAYMENT_FOR_DORMANT_USER(HttpStatus.FORBIDDEN, "CO022", "휴면 처리된 회원의 결제는 취소할 수 없습니다."),
|
||||||
|
NOT_FOUND_PAYMENT(HttpStatus.NOT_FOUND, "CO023", "존재하지 않는 결제 정보 입니다."),
|
||||||
|
FAILED_INTERNAL_SYSTEM_PROCESSING(HttpStatus.INTERNAL_SERVER_ERROR, "CO024", "내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요."),
|
||||||
|
FAILED_REFUND_PROCESS(HttpStatus.INTERNAL_SERVER_ERROR, "CO025", "은행 응답시간 지연이나 일시적인 오류로 환불요청에 실패했습니다."),
|
||||||
|
FAILED_METHOD_HANDLING_CANCEL(HttpStatus.INTERNAL_SERVER_ERROR, "CO026", "취소 중 결제 시 사용한 결제 수단 처리과정에서 일시적인 오류가 발생했습니다."),
|
||||||
|
FAILED_PARTIAL_REFUND(HttpStatus.INTERNAL_SERVER_ERROR, "CO027", "은행 점검, 해약 계좌 등의 사유로 부분 환불이 실패했습니다."),
|
||||||
|
COMMON_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "CO028", "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."),
|
||||||
|
FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(HttpStatus.INTERNAL_SERVER_ERROR, "CO029", "결제가 완료되지 않았어요. 다시 시도해주세요.");
|
||||||
|
}
|
||||||
@ -0,0 +1,57 @@
|
|||||||
|
package com.sangdol.tosspaymock.exception.code
|
||||||
|
|
||||||
|
import com.sangdol.common.types.web.HttpStatus
|
||||||
|
|
||||||
|
enum class TosspayConfirmErrorCode(
|
||||||
|
override val httpStatus: HttpStatus,
|
||||||
|
override val errorCode: String,
|
||||||
|
override val message: String,
|
||||||
|
) : TosspayErrorCode {
|
||||||
|
ALREADY_PROCESSED_PAYMENT(HttpStatus.BAD_REQUEST, "CO001", "이미 처리된 결제 입니다."),
|
||||||
|
PROVIDER_ERROR(HttpStatus.BAD_REQUEST, "CO002", "일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요."),
|
||||||
|
EXCEED_MAX_CARD_INSTALLMENT_PLAN(HttpStatus.BAD_REQUEST, "CO003", "설정 가능한 최대 할부 개월 수를 초과했습니다."),
|
||||||
|
INVALID_REQUEST(HttpStatus.BAD_REQUEST, "CO004", "잘못된 요청입니다."),
|
||||||
|
NOT_ALLOWED_POINT_USE(HttpStatus.BAD_REQUEST, "CO005", "포인트 사용이 불가한 카드로 카드 포인트 결제에 실패했습니다."),
|
||||||
|
INVALID_API_KEY(HttpStatus.BAD_REQUEST, "CO006", "잘못된 시크릿키 연동 정보 입니다."),
|
||||||
|
INVALID_REJECT_CARD(HttpStatus.BAD_REQUEST, "CO007", "카드 사용이 거절되었습니다. 카드사 문의가 필요합니다."),
|
||||||
|
BELOW_MINIMUM_AMOUNT(HttpStatus.BAD_REQUEST, "CO008", "신용카드는 결제금액이 100원 이상, 계좌는 200원이상부터 결제가 가능합니다."),
|
||||||
|
INVALID_CARD_EXPIRATION(HttpStatus.BAD_REQUEST, "CO009", "카드 정보를 다시 확인해주세요. (유효기간)"),
|
||||||
|
INVALID_STOPPED_CARD(HttpStatus.BAD_REQUEST, "CO010", "정지된 카드 입니다."),
|
||||||
|
EXCEED_MAX_DAILY_PAYMENT_COUNT(HttpStatus.BAD_REQUEST, "CO011", "하루 결제 가능 횟수를 초과했습니다."),
|
||||||
|
NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT(HttpStatus.BAD_REQUEST, "CO012", "할부가 지원되지 않는 카드 또는 가맹점 입니다."),
|
||||||
|
INVALID_CARD_INSTALLMENT_PLAN(HttpStatus.BAD_REQUEST, "CO013", "할부 개월 정보가 잘못되었습니다."),
|
||||||
|
NOT_SUPPORTED_MONTHLY_INSTALLMENT_PLAN(HttpStatus.BAD_REQUEST, "CO014", "할부가 지원되지 않는 카드입니다."),
|
||||||
|
EXCEED_MAX_PAYMENT_AMOUNT(HttpStatus.BAD_REQUEST, "CO015", "하루 결제 가능 금액을 초과했습니다."),
|
||||||
|
NOT_FOUND_TERMINAL_ID(HttpStatus.BAD_REQUEST, "CO016", "단말기번호(Terminal Id)가 없습니다. 토스페이먼츠로 문의 바랍니다."),
|
||||||
|
INVALID_AUTHORIZE_AUTH(HttpStatus.BAD_REQUEST, "CO017", "유효하지 않은 인증 방식입니다."),
|
||||||
|
INVALID_CARD_LOST_OR_STOLEN(HttpStatus.BAD_REQUEST, "CO018", "분실 혹은 도난 카드입니다."),
|
||||||
|
RESTRICTED_TRANSFER_ACCOUNT(HttpStatus.BAD_REQUEST, "CO019", "계좌는 등록 후 12시간 뒤부터 결제할 수 있습니다. 관련 정책은 해당 은행으로 문의해주세요."),
|
||||||
|
INVALID_CARD_NUMBER(HttpStatus.BAD_REQUEST, "CO020", "카드번호를 다시 확인해주세요."),
|
||||||
|
INVALID_UNREGISTERED_SUBMALL(HttpStatus.BAD_REQUEST, "CO021", "등록되지 않은 서브몰입니다. 서브몰이 없는 가맹점이라면 안심클릭이나 ISP 결제가 필요합니다."),
|
||||||
|
NOT_REGISTERED_BUSINESS(HttpStatus.BAD_REQUEST, "CO022", "등록되지 않은 사업자 번호입니다."),
|
||||||
|
EXCEED_MAX_ONE_DAY_WITHDRAW_AMOUNT(HttpStatus.BAD_REQUEST, "CO023", "1일 출금 한도를 초과했습니다."),
|
||||||
|
EXCEED_MAX_ONE_TIME_WITHDRAW_AMOUNT(HttpStatus.BAD_REQUEST, "CO024", "1회 출금 한도를 초과했습니다."),
|
||||||
|
CARD_PROCESSING_ERROR(HttpStatus.BAD_REQUEST, "CO025", "카드사에서 오류가 발생했습니다."),
|
||||||
|
EXCEED_MAX_AMOUNT(HttpStatus.BAD_REQUEST, "CO026", "거래금액 한도를 초과했습니다."),
|
||||||
|
INVALID_ACCOUNT_INFO_RE_REGISTER(HttpStatus.BAD_REQUEST, "CO027", "유효하지 않은 계좌입니다. 계좌 재등록 후 시도해주세요."),
|
||||||
|
NOT_AVAILABLE_PAYMENT(HttpStatus.BAD_REQUEST, "CO028", "결제가 불가능한 시간대입니다."),
|
||||||
|
UNAPPROVED_ORDER_ID(HttpStatus.BAD_REQUEST, "CO029", "아직 승인되지 않은 주문번호입니다."),
|
||||||
|
EXCEED_MAX_MONTHLY_PAYMENT_AMOUNT(HttpStatus.BAD_REQUEST, "CO030", "당월 결제 가능금액인 1,000,000원을 초과 하셨습니다."),
|
||||||
|
UNAUTHORIZED_KEY(HttpStatus.UNAUTHORIZED, "CO031", "인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다."),
|
||||||
|
REJECT_ACCOUNT_PAYMENT(HttpStatus.FORBIDDEN, "CO032", "잔액부족으로 결제에 실패했습니다."),
|
||||||
|
REJECT_CARD_PAYMENT(HttpStatus.FORBIDDEN, "CO033", "한도초과 혹은 잔액부족으로 결제에 실패했습니다."),
|
||||||
|
REJECT_CARD_COMPANY(HttpStatus.FORBIDDEN, "CO034", "결제 승인이 거절되었습니다."),
|
||||||
|
FORBIDDEN_REQUEST(HttpStatus.FORBIDDEN, "CO035", "허용되지 않은 요청입니다."),
|
||||||
|
REJECT_TOSSPAY_INVALID_ACCOUNT(HttpStatus.FORBIDDEN, "CO036", "선택하신 출금 계좌가 출금이체 등록이 되어 있지 않아요. 계좌를 다시 등록해 주세요."),
|
||||||
|
EXCEED_MAX_AUTH_COUNT(HttpStatus.FORBIDDEN, "CO037", "최대 인증 횟수를 초과했습니다. 카드사로 문의해주세요."),
|
||||||
|
EXCEED_MAX_ONE_DAY_AMOUNT(HttpStatus.FORBIDDEN, "CO038", "일일 한도를 초과했습니다."),
|
||||||
|
NOT_AVAILABLE_BANK(HttpStatus.FORBIDDEN, "CO039", "은행 서비스 시간이 아닙니다."),
|
||||||
|
INVALID_PASSWORD(HttpStatus.FORBIDDEN, "CO040", "결제 비밀번호가 일치하지 않습니다."),
|
||||||
|
INCORRECT_BASIC_AUTH_FORMAT(HttpStatus.FORBIDDEN, "CO041", "잘못된 요청입니다. ':' 를 포함해 인코딩해주세요."),
|
||||||
|
FDS_ERROR(HttpStatus.FORBIDDEN, "CO042", "[토스페이먼츠] 위험거래가 감지되어 결제가 제한됩니다. 발송된 문자에 포함된 링크를 통해 본인인증 후 결제가 가능합니다. (고객센터: 1644-8051)"),
|
||||||
|
NOT_FOUND_PAYMENT(HttpStatus.NOT_FOUND, "CO043", "존재하지 않는 결제 정보 입니다."),
|
||||||
|
NOT_FOUND_PAYMENT_SESSION(HttpStatus.NOT_FOUND, "CO044", "결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다."),
|
||||||
|
FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING(HttpStatus.INTERNAL_SERVER_ERROR, "CO045", "결제가 완료되지 않았어요. 다시 시도해주세요."),
|
||||||
|
FAILED_INTERNAL_SYSTEM_PROCESSING(HttpStatus.INTERNAL_SERVER_ERROR, "CO046", "내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요."),
|
||||||
|
UNKNOWN_PAYMENT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "CO047", "결제에 실패했어요. 같은 문제가 반복된다면 은행이나 카드사로 문의해주세요.");
|
||||||
|
}
|
||||||
@ -0,0 +1,9 @@
|
|||||||
|
package com.sangdol.tosspaymock.exception.code
|
||||||
|
|
||||||
|
import com.sangdol.common.types.web.HttpStatus
|
||||||
|
|
||||||
|
interface TosspayErrorCode {
|
||||||
|
val httpStatus: HttpStatus
|
||||||
|
val errorCode: String
|
||||||
|
val message: String
|
||||||
|
}
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
package com.sangdol.tosspaymock.infrastructure.persistence
|
||||||
|
|
||||||
|
import com.sangdol.common.persistence.PersistableBaseEntity
|
||||||
|
import jakarta.persistence.Column
|
||||||
|
import jakarta.persistence.Entity
|
||||||
|
import jakarta.persistence.Table
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "order_amount")
|
||||||
|
class OrderAmountEntity(
|
||||||
|
id: Long,
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
val paymentKey: String,
|
||||||
|
|
||||||
|
val approvedAmount: Int,
|
||||||
|
val easypayDiscountAmount: Int,
|
||||||
|
val cardDiscountAmount: Int,
|
||||||
|
val transferDiscountAmount: Int
|
||||||
|
) : PersistableBaseEntity(id) {
|
||||||
|
fun totalAmount(): Int {
|
||||||
|
return (approvedAmount + easypayDiscountAmount + cardDiscountAmount + transferDiscountAmount)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
package com.sangdol.tosspaymock.infrastructure.persistence
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
|
interface OrderAmountRepository : JpaRepository<OrderAmountEntity, Long> {
|
||||||
|
|
||||||
|
fun findByPaymentKey(paymentKey: String): OrderAmountEntity?
|
||||||
|
}
|
||||||
@ -0,0 +1,35 @@
|
|||||||
|
package com.sangdol.tosspaymock.web
|
||||||
|
|
||||||
|
import com.sangdol.tosspaymock.business.TosspayService
|
||||||
|
import com.sangdol.tosspaymock.web.dto.PaymentCancelRequest
|
||||||
|
import com.sangdol.tosspaymock.web.dto.PaymentConfirmRequest
|
||||||
|
import com.sangdol.tosspaymock.web.dto.PaymentResponse
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.*
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/v1/payments")
|
||||||
|
class TosspayController(
|
||||||
|
private val tosspayService: TosspayService
|
||||||
|
) {
|
||||||
|
|
||||||
|
@PostMapping("/confirm")
|
||||||
|
fun confirmPayment(
|
||||||
|
@RequestBody request: PaymentConfirmRequest
|
||||||
|
): ResponseEntity<PaymentResponse> {
|
||||||
|
val response = tosspayService.confirm(request)
|
||||||
|
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{paymentKey}/cancel")
|
||||||
|
fun cancelPayment(
|
||||||
|
@PathVariable("paymentKey") paymentKey: String,
|
||||||
|
@RequestBody request: PaymentCancelRequest
|
||||||
|
): ResponseEntity<PaymentResponse> {
|
||||||
|
val response = tosspayService.cancel(paymentKey, request)
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.sangdol.tosspaymock.web.dto
|
||||||
|
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
data class PaymentConfirmRequest(
|
||||||
|
val paymentKey: String,
|
||||||
|
val orderId: String,
|
||||||
|
val amount: Int,
|
||||||
|
val requestedAt: OffsetDateTime = OffsetDateTime.now()
|
||||||
|
)
|
||||||
|
|
||||||
|
data class PaymentCancelRequest(
|
||||||
|
val cancelAmount: Int? = null,
|
||||||
|
val cancelReason: String
|
||||||
|
)
|
||||||
@ -0,0 +1,91 @@
|
|||||||
|
package com.sangdol.tosspaymock.web.dto
|
||||||
|
|
||||||
|
import com.sangdol.tosspaymock.business.domain.Payment
|
||||||
|
import com.sangdol.tosspaymock.business.domain.PaymentType
|
||||||
|
import com.sangdol.tosspaymock.business.domain.cancel.Cancellation
|
||||||
|
import com.sangdol.tosspaymock.business.domain.card.Card
|
||||||
|
import com.sangdol.tosspaymock.business.domain.easypay.Easypay
|
||||||
|
import com.sangdol.tosspaymock.business.domain.transfer.BankTransfer
|
||||||
|
import java.time.OffsetDateTime
|
||||||
|
|
||||||
|
data class PaymentResponse(
|
||||||
|
val mid: String,
|
||||||
|
val lastTransactionKey: String,
|
||||||
|
val paymentKey: String,
|
||||||
|
val orderId: String,
|
||||||
|
val orderName: String,
|
||||||
|
val taxExemptionAmount: Int,
|
||||||
|
var status: String,
|
||||||
|
val requestedAt: OffsetDateTime,
|
||||||
|
val approvedAt: OffsetDateTime,
|
||||||
|
val useEscrow: Boolean,
|
||||||
|
val cultureExpense: Boolean,
|
||||||
|
val card: CardResponse?,
|
||||||
|
val virtualAccount: String?,
|
||||||
|
val transfer: BankTransferResponse?,
|
||||||
|
val mobilePhone: String?,
|
||||||
|
val giftCertificate: String?,
|
||||||
|
val cashReceipt: String?,
|
||||||
|
val cashReceipts: String?,
|
||||||
|
val discount: String?,
|
||||||
|
var cancels: CancelResponse?,
|
||||||
|
val secret: String?,
|
||||||
|
val type: PaymentType,
|
||||||
|
val easyPay: EasypayResponse?,
|
||||||
|
val country: String,
|
||||||
|
val failure: String?,
|
||||||
|
val isPartialCancelable: Boolean,
|
||||||
|
val receipt: String?,
|
||||||
|
val checkout: String?,
|
||||||
|
val currency: String,
|
||||||
|
val totalAmount: Int,
|
||||||
|
val balanceAmount: Int,
|
||||||
|
val suppliedAmount: Int,
|
||||||
|
val vat: Int,
|
||||||
|
val taxFreeAmount: Int,
|
||||||
|
val method: String,
|
||||||
|
val version: String,
|
||||||
|
val metadata: String?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CardResponse(
|
||||||
|
val issuerCode: String,
|
||||||
|
val acquirerCode: String,
|
||||||
|
val number: String,
|
||||||
|
val installmentPlanMonths: Int,
|
||||||
|
val isInterestFree: Boolean,
|
||||||
|
val interestPayer: String?,
|
||||||
|
val approveNo: String,
|
||||||
|
val useCardPoint: Boolean,
|
||||||
|
val cardType: String,
|
||||||
|
val ownerType: String,
|
||||||
|
val acquireStatus: String,
|
||||||
|
val amount: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class EasypayResponse(
|
||||||
|
val provider: String,
|
||||||
|
val amount: Int,
|
||||||
|
val discountAmount: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
data class BankTransferResponse(
|
||||||
|
val bankCode: String,
|
||||||
|
val settlementStatus: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CancelResponse(
|
||||||
|
val transactionKey: String,
|
||||||
|
val cancelReason: String,
|
||||||
|
val taxExemptionAmount: Int,
|
||||||
|
val canceledAt: OffsetDateTime,
|
||||||
|
val cardDiscountAmount: Int,
|
||||||
|
val transferDiscountAmount: Int,
|
||||||
|
val easyPayDiscountAmount: Int,
|
||||||
|
val receiptKey: String?,
|
||||||
|
val cancelStatus: String,
|
||||||
|
val cancelRequestId: String?,
|
||||||
|
val cancelAmount: Int,
|
||||||
|
val taxFreeAmount: Int,
|
||||||
|
val refundableAmount: Int,
|
||||||
|
)
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package com.sangdol.tosspaymock.web.supports
|
||||||
|
|
||||||
|
import com.sangdol.tosspaymock.exception.TosspayException
|
||||||
|
import com.sangdol.tosspaymock.exception.code.TosspayConfirmErrorCode
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.http.HttpHeaders
|
||||||
|
import org.springframework.stereotype.Component
|
||||||
|
import org.springframework.web.method.HandlerMethod
|
||||||
|
import org.springframework.web.servlet.HandlerInterceptor
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Component
|
||||||
|
class SecretKeyInterceptor : HandlerInterceptor {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val basicAuthRegex = Regex("Basic (.*)")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun preHandle(
|
||||||
|
request: HttpServletRequest,
|
||||||
|
response: HttpServletResponse,
|
||||||
|
handler: Any
|
||||||
|
): Boolean {
|
||||||
|
if (handler !is HandlerMethod) return true
|
||||||
|
|
||||||
|
val basicAuthSecretKey: String = request.getHeader(HttpHeaders.AUTHORIZATION)
|
||||||
|
?: throw TosspayException(TosspayConfirmErrorCode.INVALID_API_KEY)
|
||||||
|
|
||||||
|
return try {
|
||||||
|
val secretKey = basicAuthRegex.find(basicAuthSecretKey)!!.groupValues[1]
|
||||||
|
Base64.getDecoder().decode(secretKey)
|
||||||
|
|
||||||
|
true
|
||||||
|
} catch (_: Exception) {
|
||||||
|
throw TosspayException(TosspayConfirmErrorCode.UNAUTHORIZED_KEY)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
package com.sangdol.tosspaymock.web.supports
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Configuration
|
||||||
|
import org.springframework.web.servlet.config.annotation.InterceptorRegistry
|
||||||
|
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
class TosspayMvcConfig(
|
||||||
|
private val secretKeyInterceptor: SecretKeyInterceptor
|
||||||
|
) : WebMvcConfigurer {
|
||||||
|
|
||||||
|
override fun addInterceptors(registry: InterceptorRegistry) {
|
||||||
|
registry.addInterceptor(secretKeyInterceptor)
|
||||||
|
}
|
||||||
|
}
|
||||||
41
tosspay-mock/src/main/resources/application.yaml
Normal file
41
tosspay-mock/src/main/resources/application.yaml
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
server:
|
||||||
|
tomcat:
|
||||||
|
mbeanregistry:
|
||||||
|
enabled: true
|
||||||
|
forward-headers-strategy: framework
|
||||||
|
port: 8000
|
||||||
|
|
||||||
|
spring:
|
||||||
|
application:
|
||||||
|
name: tosspay-mock
|
||||||
|
jpa:
|
||||||
|
properties:
|
||||||
|
hibernate:
|
||||||
|
format_sql: true
|
||||||
|
hibernate:
|
||||||
|
ddl-auto: validate
|
||||||
|
open-in-view: false
|
||||||
|
h2:
|
||||||
|
console:
|
||||||
|
enabled: true
|
||||||
|
path: /h2-console
|
||||||
|
datasource:
|
||||||
|
hikari:
|
||||||
|
jdbc-url: jdbc:h2:mem:database
|
||||||
|
driver-class-name: org.h2.Driver
|
||||||
|
username: sa
|
||||||
|
password:
|
||||||
|
sql:
|
||||||
|
init:
|
||||||
|
mode: always
|
||||||
|
schema-locations: classpath:schema/schema-h2.sql
|
||||||
|
|
||||||
|
management:
|
||||||
|
endpoints:
|
||||||
|
web:
|
||||||
|
exposure:
|
||||||
|
include: health,loggers,prometheus
|
||||||
|
base-path: ${TOSSPAY_ACTUATOR_PATH:/actuator}
|
||||||
|
endpoint:
|
||||||
|
health:
|
||||||
|
show-details: always
|
||||||
92
tosspay-mock/src/main/resources/logback-deploy.xml
Normal file
92
tosspay-mock/src/main/resources/logback-deploy.xml
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<included>
|
||||||
|
<appender name="CONSOLE_JSON" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
|
||||||
|
<providers>
|
||||||
|
<timestamp>
|
||||||
|
<fieldName>timestamp</fieldName>
|
||||||
|
<timeZone>UTC</timeZone>
|
||||||
|
</timestamp>
|
||||||
|
<logLevel>
|
||||||
|
<fieldName>level</fieldName>
|
||||||
|
</logLevel>
|
||||||
|
<loggerName>
|
||||||
|
<fieldName>logger</fieldName>
|
||||||
|
</loggerName>
|
||||||
|
<threadName>
|
||||||
|
<fieldName>thread</fieldName>
|
||||||
|
</threadName>
|
||||||
|
<mdc/>
|
||||||
|
<pattern>
|
||||||
|
<pattern>
|
||||||
|
{
|
||||||
|
"message": "%msg"
|
||||||
|
}
|
||||||
|
</pattern>
|
||||||
|
</pattern>
|
||||||
|
<stackTrace>
|
||||||
|
<fieldName>stack_trace</fieldName>
|
||||||
|
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
|
||||||
|
<maxDepthPerThrowable>5</maxDepthPerThrowable>
|
||||||
|
<maxLength>2048</maxLength>
|
||||||
|
<rootCauseFirst>true</rootCauseFirst>
|
||||||
|
</throwableConverter>
|
||||||
|
</stackTrace>
|
||||||
|
</providers>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
|
||||||
|
<appender name="FILE_JSON" class="ch.qos.logback.core.rolling.RollingFileAppender">
|
||||||
|
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
|
||||||
|
<fileNamePattern>logs/application-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
|
||||||
|
<maxFileSize>100MB</maxFileSize>
|
||||||
|
<maxHistory>3</maxHistory>
|
||||||
|
<totalSizeCap>1GB</totalSizeCap>
|
||||||
|
</rollingPolicy>
|
||||||
|
<encoder class="net.logstash.logback.encoder.LoggingEventCompositeJsonEncoder">
|
||||||
|
<providers>
|
||||||
|
<timestamp>
|
||||||
|
<fieldName>timestamp</fieldName>
|
||||||
|
<timeZone>UTC</timeZone>
|
||||||
|
</timestamp>
|
||||||
|
<logLevel>
|
||||||
|
<fieldName>level</fieldName>
|
||||||
|
</logLevel>
|
||||||
|
<loggerName>
|
||||||
|
<fieldName>logger</fieldName>
|
||||||
|
</loggerName>
|
||||||
|
<threadName>
|
||||||
|
<fieldName>thread</fieldName>
|
||||||
|
</threadName>
|
||||||
|
<mdc/>
|
||||||
|
<pattern>
|
||||||
|
<pattern>
|
||||||
|
{
|
||||||
|
"message": "%msg"
|
||||||
|
}
|
||||||
|
</pattern>
|
||||||
|
</pattern>
|
||||||
|
<stackTrace>
|
||||||
|
<fieldName>stack_trace</fieldName>
|
||||||
|
<throwableConverter class="net.logstash.logback.stacktrace.ShortenedThrowableConverter">
|
||||||
|
<maxDepthPerThrowable>5</maxDepthPerThrowable>
|
||||||
|
<maxLength>2048</maxLength>
|
||||||
|
<rootCauseFirst>true</rootCauseFirst>
|
||||||
|
</throwableConverter>
|
||||||
|
</stackTrace>
|
||||||
|
</providers>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<appender name="ASYNC_FILE_JSON" class="ch.qos.logback.classic.AsyncAppender">
|
||||||
|
<appender-ref ref="FILE_JSON"/>
|
||||||
|
<queueSize>512</queueSize>
|
||||||
|
<discardingThreshold>0</discardingThreshold> <includeCallerData>false</includeCallerData>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="INFO">
|
||||||
|
<appender-ref ref="CONSOLE_JSON"/>
|
||||||
|
<appender-ref ref="ASYNC_FILE_JSON"/>
|
||||||
|
</root>
|
||||||
|
</included>
|
||||||
19
tosspay-mock/src/main/resources/logback-local.xml
Normal file
19
tosspay-mock/src/main/resources/logback-local.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<included>
|
||||||
|
<property name="CONSOLE_LOG_PATTERN"
|
||||||
|
value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(%-5level) %green(${PID:- }) --- [%15.15thread] [%magenta(%X{traceId:-},%X{spanId:-})] %cyan(%-40logger{36}) : %msg%n%throwable"/>
|
||||||
|
|
||||||
|
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
|
||||||
|
<encoder>
|
||||||
|
<pattern>${CONSOLE_LOG_PATTERN}</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
|
||||||
|
<root level="info">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
</root>
|
||||||
|
|
||||||
|
<logger name="com.sangdol.tosspaymock" level="info" additivity="false">
|
||||||
|
<appender-ref ref="CONSOLE"/>
|
||||||
|
</logger>
|
||||||
|
</included>
|
||||||
14
tosspay-mock/src/main/resources/logback-spring.xml
Normal file
14
tosspay-mock/src/main/resources/logback-spring.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<configuration scan="true">
|
||||||
|
<springProfile name="deploy">
|
||||||
|
<include resource="logback-deploy.xml"/>
|
||||||
|
</springProfile>
|
||||||
|
|
||||||
|
<springProfile name="local">
|
||||||
|
<include resource="logback-local.xml"/>
|
||||||
|
</springProfile>
|
||||||
|
|
||||||
|
<springProfile name="default">
|
||||||
|
<include resource="logback-local.xml"/>
|
||||||
|
</springProfile>
|
||||||
|
</configuration>
|
||||||
10
tosspay-mock/src/main/resources/schema/schema-h2.sql
Normal file
10
tosspay-mock/src/main/resources/schema/schema-h2.sql
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
create table if not exists order_amount (
|
||||||
|
id bigint primary key,
|
||||||
|
payment_key varchar(255) not null,
|
||||||
|
approved_amount integer not null,
|
||||||
|
easypay_discount_amount integer not null,
|
||||||
|
card_discount_amount integer not null,
|
||||||
|
transfer_discount_amount integer not null,
|
||||||
|
|
||||||
|
constraint uk_order_amount__payment_key unique (payment_key)
|
||||||
|
);
|
||||||
@ -0,0 +1,188 @@
|
|||||||
|
package com.sangdol.tosspaymock
|
||||||
|
|
||||||
|
import com.sangdol.common.persistence.IDGenerator
|
||||||
|
import com.sangdol.tosspaymock.exception.code.TosspayCancelErrorCode
|
||||||
|
import com.sangdol.tosspaymock.exception.code.TosspayConfirmErrorCode
|
||||||
|
import com.sangdol.tosspaymock.infrastructure.persistence.OrderAmountEntity
|
||||||
|
import com.sangdol.tosspaymock.infrastructure.persistence.OrderAmountRepository
|
||||||
|
import com.sangdol.tosspaymock.web.dto.PaymentCancelRequest
|
||||||
|
import com.sangdol.tosspaymock.web.dto.PaymentConfirmRequest
|
||||||
|
import io.kotest.core.spec.style.FunSpec
|
||||||
|
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.restassured.RestAssured
|
||||||
|
import io.restassured.module.kotlin.extensions.Extract
|
||||||
|
import io.restassured.module.kotlin.extensions.Given
|
||||||
|
import io.restassured.module.kotlin.extensions.Then
|
||||||
|
import io.restassured.module.kotlin.extensions.When
|
||||||
|
import org.hamcrest.CoreMatchers
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest
|
||||||
|
import org.springframework.boot.test.web.server.LocalServerPort
|
||||||
|
import org.springframework.http.HttpStatus
|
||||||
|
import org.springframework.http.MediaType
|
||||||
|
|
||||||
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
|
||||||
|
class TosspayApiTest(
|
||||||
|
@LocalServerPort private val port: Int,
|
||||||
|
private val orderAmountRepository: OrderAmountRepository,
|
||||||
|
private val idGenerator: IDGenerator
|
||||||
|
) : FunSpec({
|
||||||
|
|
||||||
|
beforeSpec {
|
||||||
|
RestAssured.port = port
|
||||||
|
}
|
||||||
|
|
||||||
|
afterTest {
|
||||||
|
orderAmountRepository.deleteAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
val authorizationKey = "dGVzdF9nc2tfZG9jc19PYVB6OEw1S2RtUVhrelJ6M3k0N0JNdzY6"
|
||||||
|
|
||||||
|
val paymentConfirmRequest = PaymentConfirmRequest(
|
||||||
|
paymentKey = "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1",
|
||||||
|
orderId = "MC4wODU4ODQwMzg4NDk0",
|
||||||
|
amount = 100_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
val paymentCancelRequest = PaymentCancelRequest(
|
||||||
|
cancelAmount = paymentConfirmRequest.amount,
|
||||||
|
cancelReason = "그냥.."
|
||||||
|
)
|
||||||
|
|
||||||
|
context("결제 승인") {
|
||||||
|
val paymentConfirmRequest = PaymentConfirmRequest(
|
||||||
|
paymentKey = "5EnNZRJGvaBX7zk2yd8ydw26XvwXkLrx9POLqKQjmAw4b0e1",
|
||||||
|
orderId = "MC4wODU4ODQwMzg4NDk0",
|
||||||
|
amount = 100_000,
|
||||||
|
)
|
||||||
|
|
||||||
|
test("Basic Authorization 헤더가 없으면 실패한다.") {
|
||||||
|
Given {
|
||||||
|
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
body(paymentConfirmRequest)
|
||||||
|
} When {
|
||||||
|
post("/v1/payments/confirm")
|
||||||
|
} Then {
|
||||||
|
statusCode(HttpStatus.BAD_REQUEST.value())
|
||||||
|
body("code", CoreMatchers.equalTo(TosspayConfirmErrorCode.INVALID_API_KEY.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Basic Authorization 헤더가 Base64 형식이 아니면 실패한다.") {
|
||||||
|
Given {
|
||||||
|
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
header("Authorization", "Basic hello-world")
|
||||||
|
body(paymentConfirmRequest)
|
||||||
|
} When {
|
||||||
|
post("/v1/payments/confirm")
|
||||||
|
} Then {
|
||||||
|
statusCode(HttpStatus.UNAUTHORIZED.value())
|
||||||
|
body("code", CoreMatchers.equalTo(TosspayConfirmErrorCode.UNAUTHORIZED_KEY.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("임의의 결제 정보를 반환하며, 금액은 별도로 저장한다.") {
|
||||||
|
val paymentKey = Given {
|
||||||
|
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
header("Authorization", "Basic $authorizationKey")
|
||||||
|
body(paymentConfirmRequest)
|
||||||
|
} When {
|
||||||
|
post("/v1/payments/confirm")
|
||||||
|
} Then {
|
||||||
|
statusCode(HttpStatus.OK.value())
|
||||||
|
} Extract {
|
||||||
|
path<String>("paymentKey")
|
||||||
|
}
|
||||||
|
|
||||||
|
paymentKey shouldBe paymentConfirmRequest.paymentKey
|
||||||
|
orderAmountRepository.findByPaymentKey(paymentKey).shouldNotBeNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("결제 취소") {
|
||||||
|
test("Basic Authorization 헤더가 없으면 실패한다.") {
|
||||||
|
Given {
|
||||||
|
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
body(paymentCancelRequest)
|
||||||
|
} When {
|
||||||
|
post("/v1/payments/${paymentConfirmRequest.paymentKey}/cancel")
|
||||||
|
} Then {
|
||||||
|
statusCode(HttpStatus.BAD_REQUEST.value())
|
||||||
|
body("code", CoreMatchers.equalTo(TosspayConfirmErrorCode.INVALID_API_KEY.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("Basic Authorization 헤더가 Base64 형식이 아니면 실패한다.") {
|
||||||
|
Given {
|
||||||
|
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
header("Authorization", "Basic hello-world")
|
||||||
|
body(paymentCancelRequest)
|
||||||
|
} When {
|
||||||
|
post("/v1/payments/${paymentConfirmRequest.paymentKey}/cancel")
|
||||||
|
} Then {
|
||||||
|
statusCode(HttpStatus.UNAUTHORIZED.value())
|
||||||
|
body("code", CoreMatchers.equalTo(TosspayConfirmErrorCode.UNAUTHORIZED_KEY.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
context("정상 응답") {
|
||||||
|
lateinit var orderAmount: OrderAmountEntity
|
||||||
|
|
||||||
|
beforeTest {
|
||||||
|
orderAmount = OrderAmountEntity(
|
||||||
|
id = idGenerator.create(),
|
||||||
|
paymentKey = paymentConfirmRequest.paymentKey,
|
||||||
|
approvedAmount = (paymentConfirmRequest.amount - 1000),
|
||||||
|
easypayDiscountAmount = 1000,
|
||||||
|
cardDiscountAmount = 0,
|
||||||
|
transferDiscountAmount = 0
|
||||||
|
).also {
|
||||||
|
orderAmountRepository.saveAndFlush(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("요청에 cancelAmount를 포함하지 않으면 전체 금액을 취소한다.") {
|
||||||
|
Given {
|
||||||
|
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
header("Authorization", "Basic $authorizationKey")
|
||||||
|
body(PaymentCancelRequest(cancelReason = "그냥!"))
|
||||||
|
} When {
|
||||||
|
post("/v1/payments/${paymentConfirmRequest.paymentKey}/cancel")
|
||||||
|
} Then {
|
||||||
|
statusCode(HttpStatus.OK.value())
|
||||||
|
body("paymentKey", CoreMatchers.equalTo(paymentConfirmRequest.paymentKey))
|
||||||
|
body("cancels.easyPayDiscountAmount", CoreMatchers.equalTo(orderAmount.easypayDiscountAmount))
|
||||||
|
body("cancels.cancelAmount", CoreMatchers.equalTo(orderAmount.totalAmount()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("이전 결제 정보의 할인 금액을 가져온 뒤 반환한다.") {
|
||||||
|
Given {
|
||||||
|
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
header("Authorization", "Basic $authorizationKey")
|
||||||
|
body(paymentCancelRequest)
|
||||||
|
} When {
|
||||||
|
post("/v1/payments/${paymentConfirmRequest.paymentKey}/cancel")
|
||||||
|
} Then {
|
||||||
|
statusCode(HttpStatus.OK.value())
|
||||||
|
body("paymentKey", CoreMatchers.equalTo(paymentConfirmRequest.paymentKey))
|
||||||
|
body("cancels.easyPayDiscountAmount", CoreMatchers.equalTo(orderAmount.easypayDiscountAmount))
|
||||||
|
body("cancels.cancelAmount", CoreMatchers.equalTo(paymentCancelRequest.cancelAmount))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
test("이전 결제 정보가 없으면 실패한다.") {
|
||||||
|
Given {
|
||||||
|
contentType(MediaType.APPLICATION_JSON_VALUE)
|
||||||
|
header("Authorization", "Basic $authorizationKey")
|
||||||
|
body(paymentCancelRequest)
|
||||||
|
} When {
|
||||||
|
post("/v1/payments/notExistPaymentKey/cancel")
|
||||||
|
} Then {
|
||||||
|
statusCode(HttpStatus.NOT_FOUND.value())
|
||||||
|
body("code", CoreMatchers.equalTo(TosspayCancelErrorCode.NOT_FOUND_PAYMENT.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
@ -0,0 +1,41 @@
|
|||||||
|
package com.sangdol.tosspaymock.parser.origin
|
||||||
|
|
||||||
|
import com.sangdol.common.types.web.HttpStatus
|
||||||
|
import io.kotest.core.spec.style.StringSpec
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class TosspayErrorCodeParser: StringSpec() {
|
||||||
|
init {
|
||||||
|
"Tosspay API 문서에 있는 결제 승인 에러 코드 항목을 enum의 형태로 변환한다." {
|
||||||
|
val basePath = "${File("").absolutePath}/src/test/resources"
|
||||||
|
|
||||||
|
listOf("confirm", "cancel").forEach {
|
||||||
|
val result = StringBuilder()
|
||||||
|
|
||||||
|
val lines = File("${basePath}/tosspay_${it}_errorcode.txt")
|
||||||
|
.readLines()
|
||||||
|
|
||||||
|
lines.forEachIndexed { idx, line ->
|
||||||
|
val suffix = if (idx < (lines.size - 1)) ",\n" else ";"
|
||||||
|
|
||||||
|
result.append(parseLine(idx + 1, line)).append(suffix)
|
||||||
|
}
|
||||||
|
|
||||||
|
File("${basePath}/tosspay_${it}_errorcode_parsed.txt").writeText(result.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseLine(idx: Int, text: String): String {
|
||||||
|
val regex = Regex("^(\\d+)\\t([A-Z_]+)\\t(.+[).])\\t([`A-Za-z].*)")
|
||||||
|
|
||||||
|
return regex.replace(text) { matchResult ->
|
||||||
|
val name = matchResult.groupValues[2]
|
||||||
|
val httpStatus = "HttpStatus.${HttpStatus.entries.first {it.code == matchResult.groupValues[1].trim().toInt()}.name}"
|
||||||
|
val errorCode = "CO${(idx).toString().padStart(3, '0')}"
|
||||||
|
val koreanMessage = matchResult.groupValues[3].split("\t")[0]
|
||||||
|
|
||||||
|
"$name($httpStatus, \"$errorCode\", \"$koreanMessage\")"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
29
tosspay-mock/src/test/resources/tosspay_cancel_errorcode.txt
Normal file
29
tosspay-mock/src/test/resources/tosspay_cancel_errorcode.txt
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
400 ALREADY_CANCELED_PAYMENT 이미 취소된 결제 입니다. The payment has already been canceled.
|
||||||
|
400 INVALID_REFUND_ACCOUNT_INFO 환불 계좌번호와 예금주명이 일치하지 않습니다. `accountNumber` and holderName is not matched.
|
||||||
|
400 EXCEED_CANCEL_AMOUNT_DISCOUNT_AMOUNT 즉시할인금액보다 적은 금액은 부분취소가 불가능합니다. The cancel amount cannot exceed discount amount
|
||||||
|
400 INVALID_REQUEST 잘못된 요청입니다. The bad request.
|
||||||
|
400 INVALID_REFUND_ACCOUNT_NUMBER 잘못된 환불 계좌번호입니다. Incorrect `accountNumber`
|
||||||
|
400 INVALID_BANK 유효하지 않은 은행입니다. It is an Invalid bank.
|
||||||
|
400 NOT_MATCHES_REFUNDABLE_AMOUNT 잔액 결과가 일치하지 않습니다. Balance results do not match.
|
||||||
|
400 PROVIDER_ERROR 일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요. This is temporary error. Please try again in a few minutes.
|
||||||
|
400 REFUND_REJECTED 환불이 거절됐습니다. 결제사에 문의 부탁드립니다. The refund has been rejected. Please contact the respective payment provider.
|
||||||
|
400 ALREADY_REFUND_PAYMENT 이미 환불된 결제입니다. The payment has been refunded.
|
||||||
|
400 FORBIDDEN_BANK_REFUND_REQUEST 고객 계좌가 입금이 되지 않는 상태입니다. The given bank account does not allow deposits.
|
||||||
|
401 UNAUTHORIZED_KEY 인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다. Unauthorized secretKey or clientKey.
|
||||||
|
403 NOT_CANCELABLE_AMOUNT 취소 할 수 없는 금액 입니다. This is a non-cancelable amount.
|
||||||
|
403 FORBIDDEN_CONSECUTIVE_REQUEST 반복적인 요청은 허용되지 않습니다. 잠시 후 다시 시도해주세요. Repetitive requests are not allowed. Please try again in a few minutes.
|
||||||
|
403 FORBIDDEN_REQUEST 허용되지 않은 요청입니다. Not allowed request
|
||||||
|
403 NOT_CANCELABLE_PAYMENT 취소 할 수 없는 결제 입니다. This is a non-cancelable payment.
|
||||||
|
403 EXCEED_MAX_REFUND_DUE 환불 가능한 기간이 지났습니다. Refundable date has passed.
|
||||||
|
403 NOT_ALLOWED_PARTIAL_REFUND_WAITING_DEPOSIT 입금 대기중인 결제는 부분 환불이 불가합니다. Partial refund is not available while pending deposit.
|
||||||
|
403 NOT_ALLOWED_PARTIAL_REFUND 에스크로 주문, 현금 카드 결제일 때는 부분 환불이 불가합니다. 이외 다른 결제 수단에서 부분 취소가 되지 않을 때는 토스페이먼츠에 문의해 주세요. Escrow orders or debit card payments cannot be partially canceled. If you are unable to partially cancel your order with any other payment method, please contact Tosspayments.
|
||||||
|
403 NOT_AVAILABLE_BANK 은행 서비스 시간이 아닙니다. It's not banking hour.
|
||||||
|
403 INCORRECT_BASIC_AUTH_FORMAT 잘못된 요청입니다. ':' 를 포함해 인코딩해주세요. Invalid request. Please encode including the ':' character.
|
||||||
|
403 NOT_CANCELABLE_PAYMENT_FOR_DORMANT_USER 휴면 처리된 회원의 결제는 취소할 수 없습니다. This is a non-cancelable payment. It is a dormant user account.
|
||||||
|
404 NOT_FOUND_PAYMENT 존재하지 않는 결제 정보 입니다. Not found payment
|
||||||
|
500 FAILED_INTERNAL_SYSTEM_PROCESSING 내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요. Internal system processing operation has failed. Please try again in a few minutes.
|
||||||
|
500 FAILED_REFUND_PROCESS 은행 응답시간 지연이나 일시적인 오류로 환불요청에 실패했습니다. The refund request failed due to a delay in the bank response time or a temporary error.
|
||||||
|
500 FAILED_METHOD_HANDLING_CANCEL 취소 중 결제 시 사용한 결제 수단 처리과정에서 일시적인 오류가 발생했습니다. A temporary error occurred while processing cancellation.
|
||||||
|
500 FAILED_PARTIAL_REFUND 은행 점검, 해약 계좌 등의 사유로 부분 환불이 실패했습니다. Partial refund failed due to bank check, account canceled, etc.
|
||||||
|
500 COMMON_ERROR 일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요. This is temporary error. Please try again in a few minutes.
|
||||||
|
500 FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING 결제가 완료되지 않았어요. 다시 시도해주세요. Payment has not been completed. please try again.
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
400 ALREADY_PROCESSED_PAYMENT 이미 처리된 결제 입니다. This is a payment that has already been processed.
|
||||||
|
400 PROVIDER_ERROR 일시적인 오류가 발생했습니다. 잠시 후 다시 시도해주세요. This is temporary error. Please try again in a few minutes.
|
||||||
|
400 EXCEED_MAX_CARD_INSTALLMENT_PLAN 설정 가능한 최대 할부 개월 수를 초과했습니다. Maximum number of installment months exceeded. (`installmentPlanMonths`)
|
||||||
|
400 INVALID_REQUEST 잘못된 요청입니다. The bad request.
|
||||||
|
400 NOT_ALLOWED_POINT_USE 포인트 사용이 불가한 카드로 카드 포인트 결제에 실패했습니다. Card point payment failed because the card cannot be used points.
|
||||||
|
400 INVALID_API_KEY 잘못된 시크릿키 연동 정보 입니다. Incorrect secret key.
|
||||||
|
400 INVALID_REJECT_CARD 카드 사용이 거절되었습니다. 카드사 문의가 필요합니다. Refer to card issuer/decline.
|
||||||
|
400 BELOW_MINIMUM_AMOUNT 신용카드는 결제금액이 100원 이상, 계좌는 200원이상부터 결제가 가능합니다. Payment can be made from 100 won or more by credit card, and 200 won or more for account.
|
||||||
|
400 INVALID_CARD_EXPIRATION 카드 정보를 다시 확인해주세요. (유효기간) Please check the card expiration date information again.
|
||||||
|
400 INVALID_STOPPED_CARD 정지된 카드 입니다. This is a suspended card.
|
||||||
|
400 EXCEED_MAX_DAILY_PAYMENT_COUNT 하루 결제 가능 횟수를 초과했습니다. You have exceeded the number of daily payments.
|
||||||
|
400 NOT_SUPPORTED_INSTALLMENT_PLAN_CARD_OR_MERCHANT 할부가 지원되지 않는 카드 또는 가맹점 입니다. This card or merchant does not support installment.
|
||||||
|
400 INVALID_CARD_INSTALLMENT_PLAN 할부 개월 정보가 잘못되었습니다. The installment month information is incorrect.
|
||||||
|
400 NOT_SUPPORTED_MONTHLY_INSTALLMENT_PLAN 할부가 지원되지 않는 카드입니다. This card does not support installment.
|
||||||
|
400 EXCEED_MAX_PAYMENT_AMOUNT 하루 결제 가능 금액을 초과했습니다. You have exceeded the amount you can pay per day.
|
||||||
|
400 NOT_FOUND_TERMINAL_ID 단말기번호(Terminal Id)가 없습니다. 토스페이먼츠로 문의 바랍니다. There is no Terminal Id. Please contact Toss Payments.
|
||||||
|
400 INVALID_AUTHORIZE_AUTH 유효하지 않은 인증 방식입니다. Invalid authentication
|
||||||
|
400 INVALID_CARD_LOST_OR_STOLEN 분실 혹은 도난 카드입니다. This is a lost or stolen card
|
||||||
|
400 RESTRICTED_TRANSFER_ACCOUNT 계좌는 등록 후 12시간 뒤부터 결제할 수 있습니다. 관련 정책은 해당 은행으로 문의해주세요. You can withdraw from this bank account after 12 hours from initial register. For related policies, please contact your bank.
|
||||||
|
400 INVALID_CARD_NUMBER 카드번호를 다시 확인해주세요. Please check your card number again.
|
||||||
|
400 INVALID_UNREGISTERED_SUBMALL 등록되지 않은 서브몰입니다. 서브몰이 없는 가맹점이라면 안심클릭이나 ISP 결제가 필요합니다. Not registered PG sub-mall business number.
|
||||||
|
400 NOT_REGISTERED_BUSINESS 등록되지 않은 사업자 번호입니다. Unregistered business registration number
|
||||||
|
400 EXCEED_MAX_ONE_DAY_WITHDRAW_AMOUNT 1일 출금 한도를 초과했습니다. You have exceeded the one-day withdrawal limit.
|
||||||
|
400 EXCEED_MAX_ONE_TIME_WITHDRAW_AMOUNT 1회 출금 한도를 초과했습니다. You have exceeded the one-time withdrawal limit.
|
||||||
|
400 CARD_PROCESSING_ERROR 카드사에서 오류가 발생했습니다. The card company was not able to process the request.
|
||||||
|
400 EXCEED_MAX_AMOUNT 거래금액 한도를 초과했습니다. The transaction amount limit has been exceeded.
|
||||||
|
400 INVALID_ACCOUNT_INFO_RE_REGISTER 유효하지 않은 계좌입니다. 계좌 재등록 후 시도해주세요. Invalid account. Please re-register the account and try again.
|
||||||
|
400 NOT_AVAILABLE_PAYMENT 결제가 불가능한 시간대입니다. Payment is unavailable at this time.
|
||||||
|
400 UNAPPROVED_ORDER_ID 아직 승인되지 않은 주문번호입니다. This order id has not been approved for payment.
|
||||||
|
400 EXCEED_MAX_MONTHLY_PAYMENT_AMOUNT 당월 결제 가능금액인 1,000,000원을 초과 하셨습니다. You have exceeded the allowed monthly payment amount of 1,000,000 KRW.
|
||||||
|
401 UNAUTHORIZED_KEY 인증되지 않은 시크릿 키 혹은 클라이언트 키 입니다. Unauthorized secretKey or clientKey.
|
||||||
|
403 REJECT_ACCOUNT_PAYMENT 잔액부족으로 결제에 실패했습니다. Payment declined due to insufficient balance.
|
||||||
|
403 REJECT_CARD_PAYMENT 한도초과 혹은 잔액부족으로 결제에 실패했습니다. Payment failed due to limit exceeded or insufficient balance.
|
||||||
|
403 REJECT_CARD_COMPANY 결제 승인이 거절되었습니다. Payment confirm is rejected
|
||||||
|
403 FORBIDDEN_REQUEST 허용되지 않은 요청입니다. Not allowed request
|
||||||
|
403 REJECT_TOSSPAY_INVALID_ACCOUNT 선택하신 출금 계좌가 출금이체 등록이 되어 있지 않아요. 계좌를 다시 등록해 주세요. Your account invalidated. Please register another account.
|
||||||
|
403 EXCEED_MAX_AUTH_COUNT 최대 인증 횟수를 초과했습니다. 카드사로 문의해주세요. Maximum authentication attempts exceeded.
|
||||||
|
403 EXCEED_MAX_ONE_DAY_AMOUNT 일일 한도를 초과했습니다. You have exceeded your daily limit.
|
||||||
|
403 NOT_AVAILABLE_BANK 은행 서비스 시간이 아닙니다. It's not banking hour.
|
||||||
|
403 INVALID_PASSWORD 결제 비밀번호가 일치하지 않습니다. Incorrect password
|
||||||
|
403 INCORRECT_BASIC_AUTH_FORMAT 잘못된 요청입니다. ':' 를 포함해 인코딩해주세요. Invalid request. Please encode including the ':' character.
|
||||||
|
403 FDS_ERROR [토스페이먼츠] 위험거래가 감지되어 결제가 제한됩니다. 발송된 문자에 포함된 링크를 통해 본인인증 후 결제가 가능합니다. (고객센터: 1644-8051) A fraudulent transaction has been detected. To complete the payment, complete the authentication process through the link included in the text message. (Customer Service: 1644-8051)
|
||||||
|
404 NOT_FOUND_PAYMENT 존재하지 않는 결제 정보 입니다. Not found payment
|
||||||
|
404 NOT_FOUND_PAYMENT_SESSION 결제 시간이 만료되어 결제 진행 데이터가 존재하지 않습니다. Payment session does not exist because the session time has expired.
|
||||||
|
500 FAILED_PAYMENT_INTERNAL_SYSTEM_PROCESSING 결제가 완료되지 않았어요. 다시 시도해주세요. Payment has not been completed. please try again.
|
||||||
|
500 FAILED_INTERNAL_SYSTEM_PROCESSING 내부 시스템 처리 작업이 실패했습니다. 잠시 후 다시 시도해주세요. Internal system processing operation has failed. Please try again in a few minutes.
|
||||||
|
500 UNKNOWN_PAYMENT_ERROR 결제에 실패했어요. 같은 문제가 반복된다면 은행이나 카드사로 문의해주세요. Payment failed. If the same problem occurs, please contact your bank or credit card company.
|
||||||
Loading…
x
Reference in New Issue
Block a user