diff --git a/authentication/pom.xml b/authentication/pom.xml
new file mode 100644
index 0000000..d0d26cc
--- /dev/null
+++ b/authentication/pom.xml
@@ -0,0 +1,21 @@
+
+
+ 4.0.0
+
+ com.coded.spring
+ Ordering
+ 0.0.1-SNAPSHOT
+
+
+ authentication
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+
+
+
+
+
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/AuthenticationApplication.kt b/authentication/src/main/kotlin/authentication/AuthenticationApplication.kt
new file mode 100644
index 0000000..44c3107
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/AuthenticationApplication.kt
@@ -0,0 +1,11 @@
+package authentication
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+
+@SpringBootApplication
+class AuthenticationApplication
+
+fun main(args: Array) {
+ runApplication(*args)
+}
diff --git a/authentication/src/main/kotlin/authentication/config/LoggingFilter.kt b/authentication/src/main/kotlin/authentication/config/LoggingFilter.kt
new file mode 100644
index 0000000..0dc9618
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/config/LoggingFilter.kt
@@ -0,0 +1,42 @@
+package authentication.config
+
+import jakarta.servlet.FilterChain
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import org.springframework.web.filter.OncePerRequestFilter
+import org.springframework.web.util.ContentCachingRequestWrapper
+import org.springframework.web.util.ContentCachingResponseWrapper
+
+@Component
+class LoggingFilter : OncePerRequestFilter() {
+
+ private val logger = LoggerFactory.getLogger(LoggingFilter::class.java)
+
+ override fun doFilterInternal(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ filterChain: FilterChain
+ ) {
+ val cachedRequest = ContentCachingRequestWrapper(request)
+ val cachedResponse = ContentCachingResponseWrapper(response)
+
+ filterChain.doFilter(cachedRequest, cachedResponse)
+
+ logRequest(cachedRequest)
+ logResponse(cachedResponse)
+
+ cachedResponse.copyBodyToResponse()
+ }
+
+ private fun logRequest(request: ContentCachingRequestWrapper) {
+ val requestBody = String(request.contentAsByteArray)
+ logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody")
+ }
+
+ private fun logResponse(response: ContentCachingResponseWrapper) {
+ val responseBody = String(response.contentAsByteArray)
+ logger.info("Response: status=${response.status}, body=$responseBody")
+ }
+}
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/jwt/JwtAuthenticationFilter.kt b/authentication/src/main/kotlin/authentication/jwt/JwtAuthenticationFilter.kt
new file mode 100644
index 0000000..6ba9ce3
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/jwt/JwtAuthenticationFilter.kt
@@ -0,0 +1,45 @@
+package authentication.jwt
+
+import jakarta.servlet.FilterChain
+import jakarta.servlet.http.*
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
+import org.springframework.security.core.context.SecurityContextHolder
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.web.authentication.WebAuthenticationDetailsSource
+import org.springframework.stereotype.Component
+import org.springframework.web.filter.OncePerRequestFilter
+
+@Component
+class JwtAuthenticationFilter(
+ private val jwtService: JwtService,
+ private val userDetailsService: UserDetailsService
+) : OncePerRequestFilter() {
+
+ override fun doFilterInternal(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ filterChain: FilterChain
+ ) {
+ val authHeader = request.getHeader("Authorization")
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ filterChain.doFilter(request, response)
+ return
+ }
+
+ val token = authHeader.substring(7)
+ val username = jwtService.extractUsername(token)
+
+ if (SecurityContextHolder.getContext().authentication == null) {
+ if (jwtService.isTokenValid(token, username)) {
+ val userDetails = userDetailsService.loadUserByUsername(username)
+ val authToken = UsernamePasswordAuthenticationToken(
+ userDetails, null, userDetails.authorities
+ )
+ authToken.details = WebAuthenticationDetailsSource().buildDetails(request)
+ SecurityContextHolder.getContext().authentication = authToken
+ }
+ }
+
+ filterChain.doFilter(request, response)
+ }
+}
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/jwt/JwtService.kt b/authentication/src/main/kotlin/authentication/jwt/JwtService.kt
new file mode 100644
index 0000000..a83dfb7
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/jwt/JwtService.kt
@@ -0,0 +1,50 @@
+package authentication.jwt
+
+import io.jsonwebtoken.*
+import io.jsonwebtoken.security.Keys
+import org.springframework.stereotype.Component
+import java.util.*
+import javax.crypto.SecretKey
+
+
+@Component
+class JwtService {
+
+ private val secretKey: SecretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256)
+ private val expirationMs: Long = 1000 * 60 * 60
+
+ fun generateToken(username: String): String {
+ val now = Date()
+ val expiry = Date(now.time + expirationMs)
+
+ return Jwts.builder()
+ .setSubject(username)
+ .setIssuedAt(now)
+ .setExpiration(expiry)
+ .signWith(secretKey)
+ .compact()
+ }
+
+ fun extractUsername(token: String): String {
+ return Jwts.parserBuilder()
+ .setSigningKey(secretKey)
+ .build()
+ .parseClaimsJws(token)
+ .body.subject
+ }
+
+ fun isTokenValid(token: String, username: String): Boolean {
+ return try {
+ val extractedUsername = Jwts.parserBuilder()
+ .setSigningKey(secretKey)
+ .build()
+ .parseClaimsJws(token)
+ .body
+ .subject
+
+ extractedUsername == username
+ } catch (e: Exception) {
+ false
+ }
+ }
+}
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/profile/InvalidProfileException.kt b/authentication/src/main/kotlin/authentication/profile/InvalidProfileException.kt
new file mode 100644
index 0000000..b985ade
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/profile/InvalidProfileException.kt
@@ -0,0 +1,3 @@
+package com.coded.spring.ordering.exceptions
+
+class InvalidProfileException(message: String) : Exception(message)
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/profile/ProfileController.kt b/authentication/src/main/kotlin/authentication/profile/ProfileController.kt
new file mode 100644
index 0000000..ac8a584
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/profile/ProfileController.kt
@@ -0,0 +1,63 @@
+package authentication.profile
+
+import com.coded.spring.ordering.exceptions.InvalidProfileException
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.responses.ApiResponses
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.*
+import java.security.Principal
+
+@RestController
+@RequestMapping("/profile")
+@Tag(name = "PROFILE", description = "Endpoints for managing user profiles")
+class ProfileController(
+ private val profileService: ProfileService
+) {
+
+ @PostMapping
+ @Operation(summary = "Submit user profile", description = "Creates a new profile for the logged-in user")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Profile created successfully"),
+ ApiResponse(responseCode = "400", description = "Invalid profile data or already exists"),
+ ApiResponse(responseCode = "500", description = "Internal server error")
+ ]
+ )
+ fun submitProfile(
+ @RequestBody request: ProfileRequest,
+ principal: Principal
+ ): ResponseEntity {
+ return try {
+ profileService.saveProfile(principal.name, request)
+ ResponseEntity.ok(mapOf("message" to "Profile created successfully."))
+ } catch (e: InvalidProfileException) {
+ ResponseEntity.badRequest().body(mapOf("error" to e.message))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ResponseEntity.internalServerError().body(mapOf("error" to "Something went wrong."))
+ }
+ }
+
+ @GetMapping
+ @Operation(summary = "Get user profile", description = "Fetches the profile for the logged-in user")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Profile fetched successfully"),
+ ApiResponse(responseCode = "400", description = "Profile not found or invalid request"),
+ ApiResponse(responseCode = "500", description = "Internal server error")
+ ]
+ )
+ fun getProfile(principal: Principal): ResponseEntity {
+ return try {
+ val profile = profileService.getByUsername(principal.name)
+ ResponseEntity.ok(profile)
+ } catch (e: InvalidProfileException) {
+ ResponseEntity.badRequest().body(mapOf("error" to e.message))
+ } catch (e: Exception) {
+ e.printStackTrace()
+ ResponseEntity.internalServerError().body(mapOf("error" to "Something went wrong."))
+ }
+ }
+}
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/profile/ProfileDTO.kt b/authentication/src/main/kotlin/authentication/profile/ProfileDTO.kt
new file mode 100644
index 0000000..110899b
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/profile/ProfileDTO.kt
@@ -0,0 +1,7 @@
+package authentication.profile
+
+data class ProfileRequest(
+ val firstName: String,
+ val lastName: String,
+ val phoneNumber: String
+)
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/profile/ProfileRepository.kt b/authentication/src/main/kotlin/authentication/profile/ProfileRepository.kt
new file mode 100644
index 0000000..4cec5be
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/profile/ProfileRepository.kt
@@ -0,0 +1,25 @@
+package authentication.profile
+
+import jakarta.inject.Named
+import jakarta.persistence.*
+import org.springframework.data.jpa.repository.JpaRepository
+
+@Named
+interface ProfileRepository : JpaRepository {
+ fun findByUserId(userId: Long): ProfileEntity?
+}
+
+@Entity
+@Table(name = "profiles")
+data class ProfileEntity(
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ val id: Long? = null,
+
+ val firstName: String = "",
+ val lastName: String = "",
+ val phoneNumber: String = "",
+ val userId: Long = 0
+
+)
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/profile/ProfileService.kt b/authentication/src/main/kotlin/authentication/profile/ProfileService.kt
new file mode 100644
index 0000000..7cf8fb6
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/profile/ProfileService.kt
@@ -0,0 +1,68 @@
+package authentication.profile
+
+import authentication.users.UsersRepository
+import com.coded.spring.ordering.exceptions.InvalidProfileException
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.stereotype.Service
+import org.springframework.transaction.annotation.Transactional
+
+@Service
+open class ProfileService(
+ private val profileRepository: ProfileRepository,
+ private val usersRepository: UsersRepository
+) {
+
+ fun isValidName(name: String): Boolean {
+ return name.matches(Regex("^[A-Za-z]+$"))
+ }
+
+ fun isValidPhone(phone: String): Boolean {
+ return phone.matches(Regex("^[0-9]{8}$"))
+ }
+
+ @Transactional
+ fun saveProfile(username: String, request: ProfileRequest): ProfileEntity {
+ val user = usersRepository.findByUsername(username)
+ ?: throw UsernameNotFoundException("User not found")
+
+ val existingProfile = profileRepository.findByUserId(user.id!!)
+ if (existingProfile != null) {
+ throw InvalidProfileException("Profile already exists for this user")
+ }
+
+ if (!isValidName(request.firstName)) {
+ throw InvalidProfileException("First name must contain only letters")
+ }
+
+ if (!isValidName(request.lastName)) {
+ throw InvalidProfileException("Last name must contain only letters")
+ }
+
+ if (!isValidPhone(request.phoneNumber)) {
+ throw InvalidProfileException("Phone number must be exactly 8 digits")
+ }
+
+ val profile = ProfileEntity(
+ firstName = request.firstName,
+ lastName = request.lastName,
+ phoneNumber = request.phoneNumber,
+ userId = user.id!!
+ )
+
+ return profileRepository.save(profile)
+ }
+
+ fun getByUsername(username: String): ProfileEntity {
+ val user = usersRepository.findByUsername(username)
+ if (user == null) {
+ throw UsernameNotFoundException("User not found")
+ }
+
+ val profile = profileRepository.findByUserId(user.id!!)
+ if (profile == null) {
+ throw InvalidProfileException("Profile not found for this user")
+ }
+
+ return profile
+ }
+}
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/security/AuthenticationController.kt b/authentication/src/main/kotlin/authentication/security/AuthenticationController.kt
new file mode 100644
index 0000000..54b8878
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/security/AuthenticationController.kt
@@ -0,0 +1,47 @@
+package authentication.security
+
+import authentication.jwt.JwtService
+import authentication.users.UsersService
+import io.swagger.v3.oas.annotations.*
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.security.authentication.*
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.web.bind.annotation.*
+import java.security.Principal
+
+@Tag(name = "AUTHENTICATION")
+@RestController
+@RequestMapping("/auth/v1")
+class AuthenticationController(
+ private val authenticationManager: AuthenticationManager,
+ private val userDetailsService: UserDetailsService,
+ private val jwtService: JwtService,
+ private val usersService: UsersService
+) {
+
+ @PostMapping("/login")
+ fun login(@RequestBody authRequest: AuthenticationRequest): AuthenticationResponse {
+ val authToken = UsernamePasswordAuthenticationToken(authRequest.username, authRequest.password)
+ val authentication = authenticationManager.authenticate(authToken)
+
+ if (authentication.isAuthenticated) {
+ val token = jwtService.generateToken(authRequest.username)
+ return AuthenticationResponse(token)
+ } else {
+ throw UsernameNotFoundException("Invalid login request.")
+ }
+ }
+
+ @PostMapping("/check-token")
+ fun checkToken(principal: Principal): TokenCheckResponse {
+ println("🔐 Received token for user: ${principal.name}")
+ val user = usersService.findByUsername(principal.name)
+ println("✅ Token is valid, userId=${user.id}")
+ return TokenCheckResponse(userId = user.id!!)
+ }
+}
+
+data class AuthenticationRequest(val username: String, val password: String)
+data class AuthenticationResponse(val token: String)
+data class TokenCheckResponse(val userId: Long)
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/security/CustomerUserDetailsService.kt b/authentication/src/main/kotlin/authentication/security/CustomerUserDetailsService.kt
new file mode 100644
index 0000000..103b99d
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/security/CustomerUserDetailsService.kt
@@ -0,0 +1,24 @@
+package authentication.security
+
+import org.springframework.security.core.userdetails.User
+import org.springframework.security.core.userdetails.UserDetails
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.stereotype.Service
+import authentication.users.UsersRepository
+
+@Service
+class CustomerUserDetailsService(
+ private val usersRepository: UsersRepository
+) : UserDetailsService {
+
+ override fun loadUserByUsername(username: String): UserDetails {
+ val user = usersRepository.findByUsername(username)
+ ?: throw UsernameNotFoundException("User not found")
+
+ return User.withUsername(user.username)
+ .username(user.username)
+ .password(user.password)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/security/SecurityConfig.kt b/authentication/src/main/kotlin/authentication/security/SecurityConfig.kt
new file mode 100644
index 0000000..301d536
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/security/SecurityConfig.kt
@@ -0,0 +1,57 @@
+package authentication.security
+
+import authentication.jwt.JwtAuthenticationFilter
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.authentication.AuthenticationManager
+import org.springframework.security.authentication.AuthenticationProvider
+import org.springframework.security.authentication.dao.DaoAuthenticationProvider
+import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.http.SessionCreationPolicy
+import org.springframework.security.core.userdetails.UserDetailsService
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
+
+@Configuration
+@EnableWebSecurity
+class SecurityConfig(
+ private val jwtAuthFilter: JwtAuthenticationFilter,
+ private val userDetailsService: UserDetailsService
+) {
+
+ @Bean
+ fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ http.csrf { it.disable() }
+ .authorizeHttpRequests {
+ it.requestMatchers("/auth/**").permitAll()
+ it.requestMatchers("/users/v1/register").permitAll()
+ .anyRequest().authenticated()
+ }
+ .sessionManagement {
+ it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ }
+ .authenticationProvider(authenticationProvider())
+ .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
+
+ return http.build()
+ }
+
+ @Bean
+ fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
+
+ @Bean
+ fun authenticationManager(config: AuthenticationConfiguration): AuthenticationManager =
+ config.authenticationManager
+
+ @Bean
+ fun authenticationProvider(): AuthenticationProvider {
+ val provider = DaoAuthenticationProvider()
+ provider.setUserDetailsService(userDetailsService)
+ provider.setPasswordEncoder(passwordEncoder())
+ return provider
+ }
+}
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/users/TransferFundsException.kt b/authentication/src/main/kotlin/authentication/users/TransferFundsException.kt
new file mode 100644
index 0000000..a132135
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/users/TransferFundsException.kt
@@ -0,0 +1,3 @@
+package authentication.users
+
+class TransferFundsException(msg: String) : Exception(msg)
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/users/UserDTO.kt b/authentication/src/main/kotlin/authentication/users/UserDTO.kt
new file mode 100644
index 0000000..be0e6b8
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/users/UserDTO.kt
@@ -0,0 +1,13 @@
+package authentication.users
+
+data class UserRequest(
+ val name: String,
+ val age: Int,
+ val username: String,
+ val password: String
+)
+
+data class UserResponse(
+ val id: Long,
+ val username: String
+)
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/users/UserRepository.kt b/authentication/src/main/kotlin/authentication/users/UserRepository.kt
new file mode 100644
index 0000000..a9137f9
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/users/UserRepository.kt
@@ -0,0 +1,27 @@
+package authentication.users
+
+import jakarta.inject.Named
+import jakarta.persistence.*
+import org.springframework.data.jpa.repository.JpaRepository
+
+@Named
+interface UsersRepository : JpaRepository {
+ fun age(age: Int): MutableList
+ fun findByUsername(username: String): UserEntity?
+}
+
+@Entity
+@Table(name = "users")
+data class UserEntity(
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ val id: Long? = null,
+ val name: String,
+ val age: Int,
+
+ val username: String,
+ val password: String,
+
+){
+ constructor() : this(null, "", 0,"","")
+}
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/users/UsersController.kt b/authentication/src/main/kotlin/authentication/users/UsersController.kt
new file mode 100644
index 0000000..002e4d9
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/users/UsersController.kt
@@ -0,0 +1,43 @@
+package authentication.users
+
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/users")
+@Tag(name = "AUTHENTICATION", description = "Handles user registration and listing")
+class UsersController(
+ private val usersService: UsersService
+) {
+
+ @PostMapping("/v1/register")
+ @Operation(
+ summary = "Register a new user",
+ description = "Registers a new user using username and password",
+ responses = [
+ ApiResponse(responseCode = "200", description = "User successfully registered"),
+ ApiResponse(responseCode = "400", description = "Validation failed or user already exists")
+ ]
+ )
+ fun registerUser(@RequestBody request: UserRequest): ResponseEntity {
+ return try {
+ val newUser = usersService.registerUser(request)
+ ResponseEntity.ok(newUser)
+ } catch (e: TransferFundsException) {
+ ResponseEntity.badRequest().body(mapOf("error" to e.message))
+ }
+ }
+
+ @GetMapping("/v1/list")
+ @Operation(
+ summary = "List all users",
+ description = "Returns a list of all registered users",
+ responses = [
+ ApiResponse(responseCode = "200", description = "Users listed successfully")
+ ]
+ )
+ fun users() = usersService.listUsers()
+}
\ No newline at end of file
diff --git a/authentication/src/main/kotlin/authentication/users/UsersService.kt b/authentication/src/main/kotlin/authentication/users/UsersService.kt
new file mode 100644
index 0000000..fd25a90
--- /dev/null
+++ b/authentication/src/main/kotlin/authentication/users/UsersService.kt
@@ -0,0 +1,53 @@
+package authentication.users
+
+import jakarta.inject.Named
+import org.springframework.security.core.userdetails.UsernameNotFoundException
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.stereotype.Service
+
+const val USERNAME_MIN_LENGTH = 4
+const val USERNAME_MAX_LENGTH = 30
+const val PASSWORD_MIN_LENGTH = 9
+const val PASSWORD_MAX_LENGTH = 30
+
+@Named
+@Service
+class UsersService(
+ private val usersRepository: UsersRepository,
+ private val passwordEncoder: PasswordEncoder
+) {
+
+ fun registerUser(request: UserRequest): UserResponse {
+ if (request.username.length !in USERNAME_MIN_LENGTH..USERNAME_MAX_LENGTH) {
+ throw IllegalArgumentException("Username must be between $USERNAME_MIN_LENGTH and $USERNAME_MAX_LENGTH characters")
+ }
+
+ if (request.password.length !in PASSWORD_MIN_LENGTH..PASSWORD_MAX_LENGTH) {
+ throw IllegalArgumentException("Password must be between $PASSWORD_MIN_LENGTH and $PASSWORD_MAX_LENGTH characters")
+ }
+
+ val user = UserEntity(
+ name = request.name,
+ age = request.age,
+ username = request.username,
+ password = passwordEncoder.encode(request.password)
+ )
+
+ val saved = usersRepository.save(user)
+ return UserResponse(id = saved.id!!, username = saved.username)
+ }
+
+ fun findByUsername(username: String): UserEntity {
+ return usersRepository.findByUsername(username)
+ ?: throw UsernameNotFoundException("User not found for username: $username")
+ }
+
+ fun listUsers(): List {
+ return usersRepository.findAll().map {
+ UserResponse(
+ id = it.id ?: 0,
+ username = it.username
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/authentication/src/main/resources/application.properties b/authentication/src/main/resources/application.properties
new file mode 100644
index 0000000..f66e6f8
--- /dev/null
+++ b/authentication/src/main/resources/application.properties
@@ -0,0 +1,9 @@
+spring.application.name=Kotlin.SpringbootV2
+server.port = 8081
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase
+spring.datasource.username=postgres
+spring.datasource.password=yosaka
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+
+springdoc.api-docs.path=/api-docs
diff --git a/ordering/pom.xml b/ordering/pom.xml
new file mode 100644
index 0000000..2bac8ac
--- /dev/null
+++ b/ordering/pom.xml
@@ -0,0 +1,62 @@
+
+
+ 4.0.0
+
+ com.coded.spring
+ Ordering
+ 0.0.1-SNAPSHOT
+
+
+ ordering
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.hibernate.validator
+ hibernate-validator
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+
+ org.postgresql
+ postgresql
+ runtime
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+
+ com.fasterxml.jackson.module
+ jackson-module-kotlin
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-reflect
+
+
+ org.jetbrains.kotlin
+ kotlin-stdlib-jdk8
+
+
+
+
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/OrderingApplication.kt b/ordering/src/main/kotlin/order/OrderingApplication.kt
new file mode 100644
index 0000000..c541d92
--- /dev/null
+++ b/ordering/src/main/kotlin/order/OrderingApplication.kt
@@ -0,0 +1,11 @@
+package order
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+
+@SpringBootApplication
+class OrderingApplication
+
+fun main(args: Array) {
+ runApplication(*args)
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/client/AuthenticationClient.kt b/ordering/src/main/kotlin/order/client/AuthenticationClient.kt
new file mode 100644
index 0000000..f20357a
--- /dev/null
+++ b/ordering/src/main/kotlin/order/client/AuthenticationClient.kt
@@ -0,0 +1,47 @@
+package order.client
+
+import org.springframework.core.ParameterizedTypeReference
+import org.springframework.http.*
+import org.springframework.stereotype.Component
+import org.springframework.web.client.RestTemplate
+
+data class CheckTokenResponse(val userId: Long)
+
+@Component
+class AuthenticationClient {
+
+ private val restTemplate = RestTemplate()
+ private val authServerUrl = "http://localhost:8081/auth/v1/check-token"
+
+ fun checkToken(token: String): CheckTokenResponse {
+ println("🔐 [AuthenticationClient] Checking token...")
+
+ val headers = HttpHeaders()
+ headers.setBearerAuth(token)
+ val requestEntity = HttpEntity(headers)
+
+ println("📤 Sending request to auth server at $authServerUrl with token: $token")
+
+ try {
+ val response = restTemplate.exchange(
+ authServerUrl,
+ HttpMethod.POST,
+ requestEntity,
+ object : ParameterizedTypeReference() {}
+ )
+
+ println("✅ Received response from auth server: ${response.statusCode}")
+ println("📦 Response body: ${response.body}")
+
+ if (response.statusCode != HttpStatus.OK) {
+ throw IllegalStateException("❌ Invalid token, status: ${response.statusCode}")
+ }
+
+ return response.body ?: throw IllegalStateException("❌ Missing response body from auth service")
+
+ } catch (ex: Exception) {
+ println("🔥 Exception while calling auth server: ${ex.message}")
+ throw IllegalStateException("Failed to validate token: ${ex.message}", ex)
+ }
+ }
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/config/LoggingFilter.kt b/ordering/src/main/kotlin/order/config/LoggingFilter.kt
new file mode 100644
index 0000000..fd024ee
--- /dev/null
+++ b/ordering/src/main/kotlin/order/config/LoggingFilter.kt
@@ -0,0 +1,35 @@
+package order.config
+
+import jakarta.servlet.FilterChain
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import org.springframework.web.filter.OncePerRequestFilter
+import org.springframework.web.util.ContentCachingRequestWrapper
+import org.springframework.web.util.ContentCachingResponseWrapper
+
+@Component
+class LoggingFilter : OncePerRequestFilter() {
+
+ private val logger = LoggerFactory.getLogger(LoggingFilter::class.java)
+
+ override fun doFilterInternal(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ filterChain: FilterChain
+ ) {
+ val wrappedRequest = ContentCachingRequestWrapper(request)
+ val wrappedResponse = ContentCachingResponseWrapper(response)
+
+ filterChain.doFilter(wrappedRequest, wrappedResponse)
+
+ val requestBody = String(wrappedRequest.contentAsByteArray)
+ logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody")
+
+ val responseBody = String(wrappedResponse.contentAsByteArray)
+ logger.info("Response: status=${response.status}, body=$responseBody")
+
+ wrappedResponse.copyBodyToResponse()
+ }
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/menu/MenuController.kt b/ordering/src/main/kotlin/order/menu/MenuController.kt
new file mode 100644
index 0000000..9afb089
--- /dev/null
+++ b/ordering/src/main/kotlin/order/menu/MenuController.kt
@@ -0,0 +1,28 @@
+package order.menu
+
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.responses.ApiResponses
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@Tag(name = "HOME_PAGE", description = "Endpoints related to displaying the homepage menu")
+class MenuController(
+ private val menuService: MenuService
+) {
+
+ @GetMapping("/order/menu/v1/list")
+ @Operation(
+ summary = "List all menu items for homepage",
+ description = "Returns all menu entries including discounts if active"
+ )
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Menu listed successfully"),
+ ApiResponse(responseCode = "500", description = "Server error while fetching menu")
+ ]
+ )
+ fun getMenu(): List = menuService.getMenu()
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/menu/MenuRepository.kt b/ordering/src/main/kotlin/order/menu/MenuRepository.kt
new file mode 100644
index 0000000..1f3501c
--- /dev/null
+++ b/ordering/src/main/kotlin/order/menu/MenuRepository.kt
@@ -0,0 +1,20 @@
+package order.menu
+
+import jakarta.persistence.*
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface MenuRepository : JpaRepository
+@Entity
+@Table(name = "order/menu")
+data class MenuEntity(
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ val id: Long? = null,
+ val name: String,
+ val description: String,
+ val price: Double
+) {
+ constructor(): this(null, "", "", 0.0)
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/menu/MenuService.kt b/ordering/src/main/kotlin/order/menu/MenuService.kt
new file mode 100644
index 0000000..99e2f8f
--- /dev/null
+++ b/ordering/src/main/kotlin/order/menu/MenuService.kt
@@ -0,0 +1,12 @@
+package order.menu
+
+import jakarta.inject.Named
+import order.menu.MenuEntity
+import order.menu.MenuRepository
+
+@Named
+class MenuService(
+ private val menuRepository: MenuRepository
+) {
+ fun getMenu(): List = menuRepository.findAll()
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/orders/ItemsRepository.kt b/ordering/src/main/kotlin/order/orders/ItemsRepository.kt
new file mode 100644
index 0000000..dc27389
--- /dev/null
+++ b/ordering/src/main/kotlin/order/orders/ItemsRepository.kt
@@ -0,0 +1,34 @@
+package order.orders
+
+import jakarta.persistence.*
+import org.springframework.data.jpa.repository.JpaRepository
+import org.springframework.stereotype.Repository
+
+@Repository
+interface ItemsRepository : JpaRepository{
+ fun findByOrderId(orderId: Long): List
+}
+
+@Entity
+@Table(name = "items")
+data class ItemEntity(
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ var id: Long? = null,
+
+ @Column(name = "items")
+ var name: String? = null,
+
+ var quantity: Long? = null,
+
+ var note: String? = null,
+
+ var price: Double? = null,
+
+ @Column(name = "order_id")
+ var orderId: Long? = null
+
+) {
+ constructor() : this(null, "", 1, "", 0.0, null)
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/orders/OrderController.kt b/ordering/src/main/kotlin/order/orders/OrderController.kt
new file mode 100644
index 0000000..c50dfb6
--- /dev/null
+++ b/ordering/src/main/kotlin/order/orders/OrderController.kt
@@ -0,0 +1,50 @@
+package order.orders
+
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.responses.ApiResponses
+import io.swagger.v3.oas.annotations.tags.Tag
+import jakarta.servlet.http.HttpServletRequest
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/order/orders/v1")
+@Tag(name = "ORDERING", description = "Endpoints related to order submission and retrieval")
+class OrderController(
+ private val ordersRepository: OrdersRepository,
+ private val ordersService: OrdersService
+) {
+
+ @PostMapping("/create")
+ @Operation(summary = "Create a new order", description = "Creates a new order along with items")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Order created successfully"),
+ ApiResponse(responseCode = "400", description = "Invalid input"),
+ ApiResponse(responseCode = "403", description = "Unauthorized access")
+ ]
+ )
+ fun createOrder(
+ @RequestBody request: CreateOrderRequest,
+ servletRequest: HttpServletRequest
+ ): ResponseEntity {
+ println("🔥 [OrderController] createOrder() hit")
+ val userId = servletRequest.getAttribute("userId") as Long
+ val order = ordersService.createOrder(userId, request.items)
+ return ResponseEntity.ok(order)
+ }
+
+ @GetMapping
+ @Operation(summary = "List user's orders", description = "Lists all orders submitted by the logged-in user")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Orders retrieved successfully"),
+ ApiResponse(responseCode = "403", description = "Unauthorized access")
+ ]
+ )
+ fun listMyOrders(servletRequest: HttpServletRequest): List {
+ val userId = servletRequest.getAttribute("userId") as Long
+ return ordersService.listOrdersForUser(userId)
+ }
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/orders/OrderDTO.kt b/ordering/src/main/kotlin/order/orders/OrderDTO.kt
new file mode 100644
index 0000000..9514bdc
--- /dev/null
+++ b/ordering/src/main/kotlin/order/orders/OrderDTO.kt
@@ -0,0 +1,20 @@
+package order.orders
+
+data class Item(
+ val id: Long?,
+ val orderId: Long?,
+ val name: String?,
+ val quantity: Long?,
+ val note: String?,
+ val price: Double?
+)
+
+data class Order(
+ val id: Long?,
+ val userId: Long?,
+ val items: List-
+)
+
+data class CreateOrderRequest(
+ val items: List
-
+)
diff --git a/ordering/src/main/kotlin/order/orders/OrdersRepository.kt b/ordering/src/main/kotlin/order/orders/OrdersRepository.kt
new file mode 100644
index 0000000..3acbe00
--- /dev/null
+++ b/ordering/src/main/kotlin/order/orders/OrdersRepository.kt
@@ -0,0 +1,25 @@
+package order.orders
+import jakarta.persistence.*
+import org.springframework.data.jpa.repository.JpaRepository
+
+import jakarta.inject.Named
+
+@Named
+interface OrdersRepository: JpaRepository{
+ fun findByUserId(userId: Long): List
+}
+
+@Entity
+@Table(name = "orders")
+class OrderEntity(
+
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ var id: Long? = null,
+
+ @Column(name = "user_id")
+ var userId: Long? = null,
+
+) {
+ constructor() : this(null, null)
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/orders/OrdersService.kt b/ordering/src/main/kotlin/order/orders/OrdersService.kt
new file mode 100644
index 0000000..cb2c4e2
--- /dev/null
+++ b/ordering/src/main/kotlin/order/orders/OrdersService.kt
@@ -0,0 +1,64 @@
+package order.orders
+
+import jakarta.inject.Named
+
+@Named
+class OrdersService(
+ private val ordersRepository: OrdersRepository,
+ private val itemsRepository: ItemsRepository
+) {
+
+ fun createOrder(userId: Long, items: List
- ): Order {
+ val newOrder = ordersRepository.save(OrderEntity(userId = userId))
+
+ val itemEntities = items.map {
+ ItemEntity(
+ name = it.name,
+ quantity = it.quantity,
+ note = it.note,
+ price = it.price,
+ orderId = newOrder.id!!
+ )
+ }
+
+ itemsRepository.saveAll(itemEntities)
+
+ return Order(
+ id = newOrder.id!!,
+ userId = userId,
+ items = itemEntities.map {
+ Item(
+ id = it.id,
+ orderId = it.orderId,
+ name = it.name,
+ quantity = it.quantity,
+ note = it.note,
+ price = it.price
+ )
+ }
+ )
+ }
+
+ fun listOrdersForUser(userId: Long): List {
+ return ordersRepository.findByUserId(userId).map { orderEntity ->
+ val itemEntities = itemsRepository.findByOrderId(orderEntity.id!!)
+
+ val items = itemEntities.map {
+ Item(
+ id = it.id,
+ orderId = it.orderId,
+ name = it.name,
+ quantity = it.quantity,
+ note = it.note,
+ price = it.price
+ )
+ }
+
+ Order(
+ id = orderEntity.id!!,
+ userId = userId,
+ items = items
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/security/RemoteAuthenticationFilter.kt b/ordering/src/main/kotlin/order/security/RemoteAuthenticationFilter.kt
new file mode 100644
index 0000000..581f6ac
--- /dev/null
+++ b/ordering/src/main/kotlin/order/security/RemoteAuthenticationFilter.kt
@@ -0,0 +1,46 @@
+package order.security
+
+import jakarta.servlet.FilterChain
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import order.client.AuthenticationClient
+import org.springframework.stereotype.Component
+import org.springframework.web.filter.OncePerRequestFilter
+@Component
+class RemoteAuthenticationFilter(
+ private val authenticationClient: AuthenticationClient
+) : OncePerRequestFilter() {
+
+ override fun doFilterInternal(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ filterChain: FilterChain
+ ) {
+ println("✅ RemoteAuthenticationFilter START")
+
+ val authHeader = request.getHeader("Authorization")
+ println("🟨 Authorization Header: $authHeader")
+
+ if (authHeader == null || !authHeader.startsWith("Bearer ")) {
+ println("⚠️ No Bearer token found. Skipping auth.")
+ filterChain.doFilter(request, response)
+ return
+ }
+
+ val token = authHeader.removePrefix("Bearer ").trim()
+ println("🔑 Extracted Token: $token")
+
+ try {
+ val result = authenticationClient.checkToken(token)
+ println("✅ Token validated. userId=${result.userId}")
+ request.setAttribute("userId", result.userId)
+ } catch (ex: Exception) {
+ println("❌ Token validation failed: ${ex.message}")
+ response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid token")
+ return
+ }
+
+ println("➡️ Proceeding with filter chain")
+ filterChain.doFilter(request, response)
+ }
+}
\ No newline at end of file
diff --git a/ordering/src/main/kotlin/order/security/SecurityConfig.kt b/ordering/src/main/kotlin/order/security/SecurityConfig.kt
new file mode 100644
index 0000000..556c8b1
--- /dev/null
+++ b/ordering/src/main/kotlin/order/security/SecurityConfig.kt
@@ -0,0 +1,30 @@
+package order.security
+
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.security.config.annotation.web.builders.HttpSecurity
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
+import org.springframework.security.config.http.SessionCreationPolicy
+import org.springframework.security.web.SecurityFilterChain
+import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
+
+@Configuration
+@EnableWebSecurity
+class SecurityConfig(
+ private val remoteAuthFilter: RemoteAuthenticationFilter
+) {
+
+ @Bean
+ fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
+ http.csrf { it.disable() }
+ .authorizeHttpRequests {
+ it.anyRequest().permitAll() // restrict if needed
+ }
+ .sessionManagement {
+ it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
+ }
+ .addFilterBefore(remoteAuthFilter, UsernamePasswordAuthenticationFilter::class.java)
+
+ return http.build()
+ }
+}
\ No newline at end of file
diff --git a/ordering/src/main/resources/application.properties b/ordering/src/main/resources/application.properties
new file mode 100644
index 0000000..2a25485
--- /dev/null
+++ b/ordering/src/main/resources/application.properties
@@ -0,0 +1,15 @@
+spring.application.name=Kotlin.SpringbootV2
+server.port = 8082
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase
+spring.datasource.username=postgres
+spring.datasource.password=yosaka
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+
+management.endpoints.web.exposure.include=*
+
+springdoc.api-docs.path=/api-docs
+
+logging.level.org.springframework.web=DEBUG
+logging.level.org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod=DEBUG
+spring.mvc.log-request-details=true
diff --git a/pom.xml b/pom.xml
index 163ad53..8393248 100644
--- a/pom.xml
+++ b/pom.xml
@@ -11,6 +11,7 @@
com.coded.spring
Ordering
0.0.1-SNAPSHOT
+ pom
Kotlin.SpringbootV2
Kotlin.SpringbootV2
@@ -20,6 +21,11 @@
+
+ authentication
+ ordering
+ welcome
+
@@ -31,6 +37,13 @@
1.9.25
+
+
+
+
+
+
+
org.springframework.boot
spring-boot-starter-web
@@ -58,6 +71,97 @@
kotlin-test-junit5
test
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
+ com.h2database
+ h2
+ test
+
+
+
+ jakarta.inject
+ jakarta.inject-api
+ 2.0.1
+
+
+
+ org.postgresql
+ postgresql
+ compile
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ runtime
+ 0.11.5
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ runtime
+ 0.11.5
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ test
+
+
+
+ org.springdoc
+ springdoc-openapi-starter-webmvc-api
+ 2.6.0
+
+
+
+ io.cucumber
+ cucumber-java
+ 7.11.0
+ test
+
+
+
+ io.cucumber
+ cucumber-spring
+ 7.11.0
+ test
+
+
+
+ io.cucumber
+ cucumber-junit
+ 7.11.0
+ test
+
+
+
+
+
+
+
+
diff --git a/src/main/kotlin/com/coded/spring/ordering/config/LoggingFilter.kt b/src/main/kotlin/com/coded/spring/ordering/config/LoggingFilter.kt
new file mode 100644
index 0000000..116f0a7
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/config/LoggingFilter.kt
@@ -0,0 +1,42 @@
+package com.coded.spring.ordering.config
+
+import jakarta.servlet.FilterChain
+import jakarta.servlet.http.HttpServletRequest
+import jakarta.servlet.http.HttpServletResponse
+import org.slf4j.LoggerFactory
+import org.springframework.stereotype.Component
+import org.springframework.web.filter.OncePerRequestFilter
+import org.springframework.web.util.ContentCachingRequestWrapper
+import org.springframework.web.util.ContentCachingResponseWrapper
+
+@Component
+class LoggingFilter : OncePerRequestFilter() {
+
+ private val logger = LoggerFactory.getLogger(LoggingFilter::class.java)
+
+ override fun doFilterInternal(
+ request: HttpServletRequest,
+ response: HttpServletResponse,
+ filterChain: FilterChain
+ ) {
+ val cachedRequest = ContentCachingRequestWrapper(request)
+ val cachedResponse = ContentCachingResponseWrapper(response)
+
+ filterChain.doFilter(cachedRequest, cachedResponse)
+
+ logRequest(cachedRequest)
+ logResponse(cachedResponse)
+
+ cachedResponse.copyBodyToResponse()
+ }
+
+ private fun logRequest(request: ContentCachingRequestWrapper) {
+ val requestBody = String(request.contentAsByteArray)
+ logger.info("Request: method=${request.method}, uri=${request.requestURI}, body=$requestBody")
+ }
+
+ private fun logResponse(response: ContentCachingResponseWrapper) {
+ val responseBody = String(response.contentAsByteArray)
+ logger.info("Response: status=${response.status}, body=$responseBody")
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/exceptions/TransferFundsException.kt b/src/main/kotlin/com/coded/spring/ordering/exceptions/TransferFundsException.kt
new file mode 100644
index 0000000..4fccd82
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/exceptions/TransferFundsException.kt
@@ -0,0 +1,3 @@
+package com.coded.spring.ordering.exceptions
+
+class TransferFundsException(msg: String) : Exception(msg)
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemDTO.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemDTO.kt
new file mode 100644
index 0000000..b0db87e
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemDTO.kt
@@ -0,0 +1,17 @@
+package items
+
+data class Item(
+ val id: Long?,
+ val orderId: Long?,
+ val name: String?,
+ val quantity: Long?,
+ val note: String?,
+ val price: Double?
+)
+
+data class SubmitItemRequest(
+ val name: String,
+ val quantity: Long,
+ val note: String?,
+ val price: Double
+)
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt
new file mode 100644
index 0000000..a8835b7
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsController.kt
@@ -0,0 +1,49 @@
+package items
+
+import jakarta.servlet.http.HttpServletRequest
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.responses.ApiResponses
+import io.swagger.v3.oas.annotations.tags.Tag
+import client.AuthenticationClient
+import orders.ItemEntity
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@Tag(name = "ORDERING", description = "Endpoints related to managing menu items")
+class ItemsController(
+ private val itemsService: ItemsService,
+ private val authenticationClient: AuthenticationClient
+) {
+
+ @GetMapping("/listItems")
+ @Operation(summary = "List all menu items", description = "Returns a list of all available items.")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Items listed successfully"),
+ ApiResponse(responseCode = "500", description = "Server error while retrieving items")
+ ]
+ )
+ fun listItems(): List
- = itemsService.listItems()
+
+ @PostMapping("/submitItems")
+ @Operation(summary = "Submit a new menu item", description = "Allows authenticated users to add a new item to the menu.")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Item submitted successfully"),
+ ApiResponse(responseCode = "400", description = "Invalid item data"),
+ ApiResponse(responseCode = "401", description = "Unauthorized request")
+ ]
+ )
+ fun submitItem(
+ @RequestBody request: SubmitItemRequest,
+ servletRequest: HttpServletRequest
+ ): ItemEntity {
+ val authHeader = servletRequest.getHeader("Authorization")
+ ?: throw IllegalStateException("Missing Authorization header")
+
+ authenticationClient.checkToken(authHeader.removePrefix("Bearer ").trim())
+
+ return itemsService.submitItem(request)
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt b/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt
new file mode 100644
index 0000000..0b7c85e
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/items/ItemsService.kt
@@ -0,0 +1,40 @@
+package items
+
+import orders.ItemEntity
+import orders.ItemsRepository
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Service
+
+@Service
+class ItemsService(
+ private val itemsRepository: ItemsRepository,
+ @Value("\${festive-mode:false}")
+ private val festiveMode: Boolean
+) {
+
+ fun listItems(): List
- = itemsRepository.findAll().map { entity ->
+ Item(
+ id = entity.id,
+ orderId = entity.orderId,
+ name = entity.name,
+ quantity = entity.quantity,
+ note = entity.note,
+ price = calculatePrice(entity.price)
+ )
+ }
+
+ fun submitItem(request: SubmitItemRequest): ItemEntity {
+ val item = ItemEntity(
+ name = request.name,
+ quantity = request.quantity ?: 0,
+ note = request.note,
+ price = request.price ?: 0.0,
+ orderId = 0 // Default to 0, actual value set on order placement
+ )
+ return itemsRepository.save(item)
+ }
+
+ private fun calculatePrice(originalPrice: Double?): Double? {
+ return if (festiveMode) originalPrice?.times(0.8) else originalPrice
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/script/InitUserRunner.kt b/src/main/kotlin/com/coded/spring/ordering/script/InitUserRunner.kt
new file mode 100644
index 0000000..67a8062
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/script/InitUserRunner.kt
@@ -0,0 +1,32 @@
+package com.coded.spring.ordering.script
+
+import com.coded.spring.ordering.users.UserEntity
+import com.coded.spring.ordering.users.UsersRepository
+import org.springframework.boot.CommandLineRunner
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.context.annotation.Bean
+import org.springframework.security.crypto.password.PasswordEncoder
+
+@SpringBootApplication
+class InitUserRunner {
+ @Bean
+ fun initUsers(userRepository: UsersRepository, passwordEncoder: PasswordEncoder) = CommandLineRunner {
+ val user = UserEntity(
+ name = "Ahmed",
+ username = "Ahmed123",
+ password = passwordEncoder.encode("password123"),
+ age = 22,
+ )
+ if (userRepository.findByUsername(user.username) == null) {
+ println("Creating user ${user.username}")
+ userRepository.save(user)
+ } else {
+ println("User ${user.username} already exists")
+ }
+ }
+}
+
+fun main(args: Array) {
+ runApplication(*args).close()
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt
new file mode 100644
index 0000000..79fb067
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/users/UserRepository.kt
@@ -0,0 +1,27 @@
+package com.coded.spring.ordering.users
+
+import jakarta.inject.Named
+import jakarta.persistence.*
+import org.springframework.data.jpa.repository.JpaRepository
+
+@Named
+interface UsersRepository : JpaRepository {
+ fun age(age: Int): MutableList
+ fun findByUsername(username: String): UserEntity?
+}
+
+@Entity
+@Table(name = "users")
+data class UserEntity(
+ @Id
+ @GeneratedValue(strategy = GenerationType.IDENTITY)
+ val id: Long? = null,
+ val name: String,
+ val age: Int,
+
+ val username: String,
+ val password: String,
+
+){
+ constructor() : this(null, "", 0,"","")
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt
new file mode 100644
index 0000000..74bbbad
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersController.kt
@@ -0,0 +1,45 @@
+package com.coded.spring.ordering.users
+
+import com.coded.spring.ordering.DTO.UserRequest
+import com.coded.spring.ordering.exceptions.TransferFundsException
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.http.ResponseEntity
+import org.springframework.web.bind.annotation.*
+
+@RestController
+@RequestMapping("/users")
+@Tag(name = "AUTHENTICATION", description = "Handles user registration and listing")
+class UsersController(
+ private val usersService: UsersService
+) {
+
+ @PostMapping("/v1/register")
+ @Operation(
+ summary = "Register a new user",
+ description = "Registers a new user using username and password",
+ responses = [
+ ApiResponse(responseCode = "200", description = "User successfully registered"),
+ ApiResponse(responseCode = "400", description = "Validation failed or user already exists")
+ ]
+ )
+ fun registerUser(@RequestBody request: UserRequest): ResponseEntity {
+ return try {
+ val newUser = usersService.registerUser(request)
+ ResponseEntity.ok(newUser)
+ } catch (e: TransferFundsException) {
+ ResponseEntity.badRequest().body(mapOf("error" to e.message))
+ }
+ }
+
+ @GetMapping("/v1/list")
+ @Operation(
+ summary = "List all users",
+ description = "Returns a list of all registered users",
+ responses = [
+ ApiResponse(responseCode = "200", description = "Users listed successfully")
+ ]
+ )
+ fun users() = usersService.listUsers()
+}
\ No newline at end of file
diff --git a/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt
new file mode 100644
index 0000000..e38cc74
--- /dev/null
+++ b/src/main/kotlin/com/coded/spring/ordering/users/UsersService.kt
@@ -0,0 +1,57 @@
+package com.coded.spring.ordering.users
+import com.coded.spring.ordering.DTO.UserRequest
+import com.coded.spring.ordering.DTO.UserResponse
+import com.coded.spring.ordering.exceptions.TransferFundsException
+import jakarta.inject.Named
+import org.springframework.security.crypto.password.PasswordEncoder
+import org.springframework.stereotype.Service
+
+const val USERNAME_MIN_LENGTH = 4
+const val USERNAME_MAX_LENGTH = 30
+const val PASSWORD_MIN_LENGTH = 9
+const val PASSWORD_MAX_LENGTH = 30
+
+@Named
+@Service
+class UsersService(
+ private val usersRepository: UsersRepository,
+ private val passwordEncoder: PasswordEncoder
+) {
+
+ fun registerUser(request: UserRequest): UserResponse {
+
+ if (request.username.length < USERNAME_MIN_LENGTH ||
+ request.username.length > USERNAME_MAX_LENGTH) {
+ throw TransferFundsException(
+ "Username must be between ${USERNAME_MIN_LENGTH} and ${USERNAME_MAX_LENGTH} characters")
+ }
+
+ if (request.password.length < PASSWORD_MIN_LENGTH ||
+ request.password.length > PASSWORD_MAX_LENGTH) {
+ throw TransferFundsException(
+ "Password must be between ${PASSWORD_MIN_LENGTH} and ${PASSWORD_MAX_LENGTH} characters")
+ }
+
+ val encodedPassword = passwordEncoder.encode(request.password)
+
+ val createUser = UserEntity(
+ name = request.name,
+ age = request.age,
+ username = request.username,
+ password = request.password
+ )
+
+ val savedUser = usersRepository.save(createUser)
+ return UserResponse(id = savedUser.id!!, username = savedUser.username)
+ }
+
+ fun listUsers(): List = usersRepository.findAll().map {
+ UserRequest(
+ name = it.name,
+ age = it.age,
+ username = it.username,
+ password = it.password
+
+ )
+ }
+}
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index 3704dc6..bf02763 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -1 +1,11 @@
spring.application.name=Kotlin.SpringbootV2
+server.port = 8080
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase
+spring.datasource.username=postgres
+spring.datasource.password=yosaka
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+
+springdoc.api-docs.path=/api-docs
+
+company.name=FreshEats
diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html
new file mode 100644
index 0000000..08232a8
--- /dev/null
+++ b/src/main/resources/static/index.html
@@ -0,0 +1,10 @@
+
+
+
+
+ Welcome to SpeedDash!
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt
index b2e2320..effb76e 100644
--- a/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt
+++ b/src/test/kotlin/com/coded/spring/ordering/ApplicationTests.kt
@@ -1,13 +1,17 @@
package com.coded.spring.ordering
+import io.cucumber.spring.CucumberContextConfiguration
import org.junit.jupiter.api.Test
import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.test.context.ActiveProfiles
-@SpringBootTest
-class ApplicationTests {
+@CucumberContextConfiguration
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+@ActiveProfiles("test")
+class OnlineOrderingApplicationTests {
@Test
fun contextLoads() {
- }
+ }
}
diff --git a/src/test/kotlin/com/coded/spring/ordering/UserIntegrationTest.kt b/src/test/kotlin/com/coded/spring/ordering/UserIntegrationTest.kt
new file mode 100644
index 0000000..f192467
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/UserIntegrationTest.kt
@@ -0,0 +1,42 @@
+package com.coded.spring.ordering
+
+import org.junit.jupiter.api.Test
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.*
+import kotlin.test.assertEquals
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class UserRequestIntegrationTest {
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+
+ @Test
+ fun createUser() {
+ val user = mapOf(
+ "name" to "Ali",
+ "age" to 23,
+ "username" to "Ali123",
+ "password" to "password123"
+ )
+
+ val headers = HttpHeaders()
+ headers.contentType = MediaType.APPLICATION_JSON
+
+ val response = restTemplate.postForEntity(
+ "/users/v1/register",
+ HttpEntity(user, headers),
+ String::class.java
+ )
+
+ assertEquals(HttpStatus.OK, response.statusCode)
+ }
+
+ @Test
+ fun getUsers() {
+ val response = restTemplate.getForEntity("/users/v1/list", String::class.java)
+ assertEquals(HttpStatus.OK, response.statusCode)
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/config/TestHooks.kt b/src/test/kotlin/com/coded/spring/ordering/config/TestHooks.kt
new file mode 100644
index 0000000..433c778
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/config/TestHooks.kt
@@ -0,0 +1,71 @@
+package com.coded.spring.ordering.config
+
+import com.coded.spring.ordering.utils.GlobalToken
+import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
+import io.cucumber.java.Before
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.*
+
+class TestHooks {
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+
+ companion object {
+ private var isTokenGenerated = false
+ }
+
+ @Before
+ fun generateJwtToken() {
+ if (isTokenGenerated) return
+
+ println("🚀 Generating Global JWT Token...")
+
+ val headers = HttpHeaders()
+ headers.contentType = MediaType.APPLICATION_JSON
+
+ // Register
+ val registrationPayload = """
+ {
+ "name": "Global User",
+ "age": 25,
+ "username": "GlobalUser",
+ "password": "password123"
+ }
+ """.trimIndent()
+ val registerRequest = HttpEntity(registrationPayload, headers)
+ var regResponse =restTemplate.postForEntity("/users/v1/register", registerRequest, String::class.java)
+
+ println(regResponse)
+ // Login
+ val loginPayload = """
+ {
+ "username": "GlobalUser",
+ "password": "password123"
+ }
+ """.trimIndent()
+ val loginRequest = HttpEntity(loginPayload, headers)
+ val response = restTemplate.postForEntity("/auth/login", loginRequest, String::class.java)
+
+ println("🔎 Login Response Body: ${response.body}")
+
+ // Extract JWT token from response JSON
+ val responseBody = response.body
+ val token = if (!responseBody.isNullOrBlank()) {
+ val mapper = jacksonObjectMapper()
+ try {
+ mapper.readTree(responseBody).get("token")?.asText()
+ } catch (e: Exception) {
+ println("❌ Failed to extract token: ${e.message}")
+ null
+ }
+ } else {
+ null
+ }
+
+ GlobalToken.jwtToken = token
+ println("✅ Global JWT Token Generated: $token")
+ isTokenGenerated = true
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/GetProfileSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/GetProfileSteps.kt
new file mode 100644
index 0000000..5940daa
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/steps/GetProfileSteps.kt
@@ -0,0 +1,50 @@
+package com.coded.spring.ordering.steps
+
+import com.coded.spring.ordering.DTO.ProfileRequest
+import com.coded.spring.ordering.authentication.jwt.JwtService
+import io.cucumber.java.en.Given
+import io.cucumber.java.en.Then
+import io.cucumber.java.en.When
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.*
+import kotlin.test.assertEquals
+
+class GetProfileSteps {
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+
+ @Autowired
+ lateinit var jwtService: JwtService
+
+ lateinit var headers: HttpHeaders
+ lateinit var response: ResponseEntity
+
+ @Given("A profile is already created for user {string}")
+ fun a_profile_is_already_created(username: String) {
+ headers = HttpHeaders()
+ headers.contentType = MediaType.APPLICATION_JSON
+ headers.setBearerAuth(jwtService.generateToken(username))
+
+ val profileRequest = ProfileRequest(
+ firstName = "John",
+ lastName = "Doe",
+ phoneNumber = "12345678"
+ )
+
+ val requestEntity = HttpEntity(profileRequest, headers)
+ restTemplate.postForEntity("/profile", requestEntity, String::class.java)
+ }
+
+ @When("I send an authenticated GET request to {string} to retrieve profile")
+ fun i_send_authenticated_get_request(url: String) {
+ val entity = HttpEntity(headers)
+ response = restTemplate.exchange(url, HttpMethod.GET, entity, String::class.java)
+ }
+
+ @Then("I should receive a 200 response for retrieving profile")
+ fun i_should_receive_200_response_for_retrieving_profile() {
+ assertEquals(200, response.statusCode.value())
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/ListItemsStepDefinitions.kt b/src/test/kotlin/com/coded/spring/ordering/steps/ListItemsStepDefinitions.kt
new file mode 100644
index 0000000..4afbfac
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/steps/ListItemsStepDefinitions.kt
@@ -0,0 +1,34 @@
+package com.coded.spring.ordering.steps
+
+import com.coded.spring.ordering.authentication.jwt.JwtService
+import io.cucumber.java.en.Then
+import io.cucumber.java.en.When
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.*
+import kotlin.test.assertEquals
+
+class ListItemsSteps {
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+
+ @Autowired
+ lateinit var jwtService: JwtService
+
+ lateinit var response: ResponseEntity
+
+ @When("I send a GET request to {string}")
+ fun i_send_a_get_request_to(url: String) {
+ val headers = HttpHeaders()
+ headers.setBearerAuth(jwtService.generateToken("GlobalUser")) // ✅ JWT Token injected
+ val entity = HttpEntity(headers)
+
+ response = restTemplate.exchange(url, HttpMethod.GET, entity, String::class.java)
+ }
+
+ @Then("I should receive a {int} response for listing items")
+ fun i_should_receive_a_response_for_listing_items(status: Int) {
+ assertEquals(status, response.statusCode.value())
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/ListOrdersStepDefinitions.kt b/src/test/kotlin/com/coded/spring/ordering/steps/ListOrdersStepDefinitions.kt
new file mode 100644
index 0000000..7eb47fe
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/steps/ListOrdersStepDefinitions.kt
@@ -0,0 +1,34 @@
+package com.coded.spring.ordering.steps
+
+import com.coded.spring.ordering.authentication.jwt.JwtService
+import io.cucumber.java.en.Then
+import io.cucumber.java.en.When
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.*
+import kotlin.test.assertEquals
+
+class ListOrdersStepDefinitions {
+
+ @Autowired
+ lateinit var jwtService: JwtService
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+
+ lateinit var getResponse: ResponseEntity
+
+ @When("I request my orders from {string}")
+ fun i_request_orders(url: String) {
+ val headers = HttpHeaders()
+ headers.setBearerAuth(jwtService.generateToken("GlobalUser"))
+ val entity = HttpEntity(headers)
+
+ getResponse = restTemplate.exchange(url, HttpMethod.GET, entity, String::class.java)
+ }
+
+ @Then("I should receive a 200 response with my orders")
+ fun i_should_receive_orders_response() {
+ assertEquals(200, getResponse.statusCode.value())
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/ListUsersStepDefinitions.kt b/src/test/kotlin/com/coded/spring/ordering/steps/ListUsersStepDefinitions.kt
new file mode 100644
index 0000000..bf16063
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/steps/ListUsersStepDefinitions.kt
@@ -0,0 +1,34 @@
+package com.coded.spring.ordering.steps
+
+import com.coded.spring.ordering.authentication.jwt.JwtService
+import io.cucumber.java.en.Then
+import io.cucumber.java.en.When
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.*
+import kotlin.test.assertEquals
+
+class ListUsersStepDefinitions {
+
+ @Autowired
+ lateinit var jwtService: JwtService
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+ lateinit var response: ResponseEntity
+
+ @When("I send an authenticated GET request to {string}")
+ fun i_send_authenticated_get_request(url: String) {
+ val headers = HttpHeaders()
+ //GlobalToken.jwtToken?.let { headers.setBearerAuth(it) }
+ headers.setBearerAuth(jwtService.generateToken("GlobalUser"))
+ val entity = HttpEntity(headers)
+
+ response = restTemplate.exchange(url, HttpMethod.GET, entity, String::class.java)
+ }
+
+ @Then("I should receive a 200 response for listing users")
+ fun i_should_receive_200_response_for_listing_users() {
+ assertEquals(200, response.statusCode.value())
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/MenuStepDefinitions.kt b/src/test/kotlin/com/coded/spring/ordering/steps/MenuStepDefinitions.kt
new file mode 100644
index 0000000..eeb42a1
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/steps/MenuStepDefinitions.kt
@@ -0,0 +1,31 @@
+package com.coded.spring.ordering.steps
+
+import io.cucumber.java.en.Then
+import io.cucumber.java.en.When
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.context.SpringBootTest
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.ResponseEntity
+import kotlin.test.assertEquals
+import kotlin.test.assertNotNull
+
+@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+class MenuStepDefinitions {
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+
+ private lateinit var response: ResponseEntity
+
+ @When("I request the menu")
+ fun iRequestTheMenu() {
+ response = restTemplate.getForEntity("/menu/v1/list", String::class.java)
+ }
+
+ @Then("I should receive a list of menu items")
+ fun iShouldReceiveMenuItems() {
+ assertEquals(200, response.statusCode.value())
+ assertNotNull(response.body)
+ println("✅ Menu Response: ${response.body}")
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/SubmitItemSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitItemSteps.kt
new file mode 100644
index 0000000..a76adb3
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitItemSteps.kt
@@ -0,0 +1,48 @@
+package com.coded.spring.ordering.steps
+
+import com.coded.spring.ordering.DTO.SubmitItemRequest
+import io.cucumber.java.en.Given
+import io.cucumber.java.en.Then
+import io.cucumber.java.en.When
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.*
+import kotlin.test.assertEquals
+import com.coded.spring.ordering.authentication.jwt.JwtService
+
+
+class SubmitItemSteps {
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+
+ @Autowired
+ lateinit var jwtService: JwtService
+
+ lateinit var requestEntity: HttpEntity
+ lateinit var response: ResponseEntity
+
+ @Given("I have an item with name {string}, quantity {int}, note {string}, and price {double}")
+ fun i_have_an_item(name: String, quantity: Int, note: String, price: Double) {
+ val itemRequest = SubmitItemRequest(
+ name = name,
+ quantity = quantity.toLong(),
+ note = note,
+ price = price
+ )
+ val headers = HttpHeaders()
+ headers.contentType = MediaType.APPLICATION_JSON
+ headers.setBearerAuth(jwtService.generateToken("GlobalUser")) // ✅ JWT Injection here
+ requestEntity = HttpEntity(itemRequest, headers)
+ }
+
+ @When("I submit an item to endpoint {string}")
+ fun i_send_post_request(url: String) {
+ response = restTemplate.postForEntity(url, requestEntity, String::class.java)
+ }
+
+ @Then("I should receive a 200 response for submitting item")
+ fun i_should_receive_success() {
+ assertEquals(200, response.statusCode.value())
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/SubmitOrderStepDefinitions.kt b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitOrderStepDefinitions.kt
new file mode 100644
index 0000000..b282950
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitOrderStepDefinitions.kt
@@ -0,0 +1,41 @@
+package com.coded.spring.ordering.steps
+
+import com.coded.spring.ordering.authentication.jwt.JwtService
+import io.cucumber.java.en.Then
+import io.cucumber.java.en.When
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.*
+import kotlin.test.assertEquals
+
+class SubmitOrderStepDefinitions {
+
+ @Autowired
+ lateinit var jwtService: JwtService
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+
+ lateinit var postResponse: ResponseEntity
+
+ @When("I submit an order with itemIds to {string}")
+ fun i_submit_order(url: String) {
+ val headers = HttpHeaders()
+ headers.contentType = MediaType.APPLICATION_JSON
+ headers.setBearerAuth(jwtService.generateToken("GlobalUser"))
+
+ val orderPayload = """
+ {
+ "itemIds": [1, 2]
+ }
+ """.trimIndent()
+
+ val request = HttpEntity(orderPayload, headers)
+ postResponse = restTemplate.postForEntity(url, request, String::class.java)
+ }
+
+ @Then("I should receive a 200 response for order submission")
+ fun i_should_receive_200_response_for_submission() {
+ assertEquals(200, postResponse.statusCode.value())
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/SubmitProfileSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitProfileSteps.kt
new file mode 100644
index 0000000..bd53c3a
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/steps/SubmitProfileSteps.kt
@@ -0,0 +1,48 @@
+package com.coded.spring.ordering.steps
+
+import com.coded.spring.ordering.DTO.ProfileRequest
+import com.coded.spring.ordering.authentication.jwt.JwtService
+import io.cucumber.java.en.Given
+import io.cucumber.java.en.Then
+import io.cucumber.java.en.When
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.*
+import kotlin.test.assertEquals
+
+class SubmitProfileSteps {
+
+ @Autowired
+ lateinit var jwtService: JwtService
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+
+ lateinit var requestEntity: HttpEntity
+ lateinit var response: ResponseEntity
+
+ @Given("I have a profile with first name {string}, last name {string}, and phone number {string}")
+ fun i_have_a_profile(firstName: String, lastName: String, phoneNumber: String) {
+ val profileRequest = ProfileRequest(
+ firstName = firstName,
+ lastName = lastName,
+ phoneNumber = phoneNumber
+ )
+
+ val headers = HttpHeaders()
+ headers.contentType = MediaType.APPLICATION_JSON
+ headers.setBearerAuth(jwtService.generateToken("GlobalUser"))
+
+ requestEntity = HttpEntity(profileRequest, headers)
+ }
+
+ @When("I submit the profile to {string}")
+ fun i_submit_the_profile(url: String) {
+ response = restTemplate.postForEntity(url, requestEntity, String::class.java)
+ }
+
+ @Then("I should receive a 200 response for submitting profile")
+ fun i_should_receive_200_response_for_profile() {
+ assertEquals(200, response.statusCode.value())
+ }
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/steps/UserRegistrationSteps.kt b/src/test/kotlin/com/coded/spring/ordering/steps/UserRegistrationSteps.kt
new file mode 100644
index 0000000..18ff93d
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/steps/UserRegistrationSteps.kt
@@ -0,0 +1,45 @@
+package com.coded.spring.ordering.steps
+
+import io.cucumber.java.en.Given
+import io.cucumber.java.en.When
+import io.cucumber.java.en.Then
+import org.springframework.beans.factory.annotation.Autowired
+import org.springframework.boot.test.web.client.TestRestTemplate
+import org.springframework.http.*
+import kotlin.test.assertEquals
+
+class UserRegistrationSteps {
+
+ @Autowired
+ lateinit var restTemplate: TestRestTemplate
+
+ private lateinit var response: ResponseEntity
+ private lateinit var requestEntity: HttpEntity
+
+ @Given("I have a user with name {string}, age {int}, username {string}, and password {string}")
+ fun i_have_a_user_payload(name: String, age: Int, username: String, password: String) {
+ val jsonPayload = """
+ {
+ "name": "$name",
+ "age": $age,
+ "username": "$username",
+ "password": "$password"
+ }
+ """.trimIndent()
+
+ val headers = HttpHeaders()
+ headers.contentType = MediaType.APPLICATION_JSON
+ requestEntity = HttpEntity(jsonPayload, headers)
+ }
+
+ @When("I send a POST request to {string}")
+ fun i_send_post_request(url: String) {
+ response = restTemplate.postForEntity(url, requestEntity, String::class.java)
+ }
+
+ @Then("I should receive a {int} response")
+ fun i_should_receive_http_code(httpCode: Int) {
+ assertEquals(httpCode, response.statusCode.value())
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/kotlin/com/coded/spring/ordering/utils/GlobalToken.kt b/src/test/kotlin/com/coded/spring/ordering/utils/GlobalToken.kt
new file mode 100644
index 0000000..1aaeae7
--- /dev/null
+++ b/src/test/kotlin/com/coded/spring/ordering/utils/GlobalToken.kt
@@ -0,0 +1,5 @@
+package com.coded.spring.ordering.utils
+
+object GlobalToken {
+ var jwtToken: String? = null
+}
\ No newline at end of file
diff --git a/src/test/kotlin/resources/application-test.properties b/src/test/kotlin/resources/application-test.properties
new file mode 100644
index 0000000..15722c5
--- /dev/null
+++ b/src/test/kotlin/resources/application-test.properties
@@ -0,0 +1,14 @@
+#spring.application.name=Kotlin.SpringbootV2
+#
+## H2 In-Memory DB for Testing
+#spring.datasource.url=jdbc:h2:mem:testdb
+#spring.datasource.driverClassName=org.h2.Driver
+#spring.datasource.username=sa
+#spring.datasource.password=
+#spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
+#spring.jpa.hibernate.ddl-auto=create-drop
+#spring.jpa.show-sql=true
+#
+## Enable H2 Console for Debugging
+#spring.h2.console.enabled=true
+#spring.h2.console.path=/h2-console
\ No newline at end of file
diff --git a/src/test/kotlin/resources/feature/get_profile.feature b/src/test/kotlin/resources/feature/get_profile.feature
new file mode 100644
index 0000000..0387ba9
--- /dev/null
+++ b/src/test/kotlin/resources/feature/get_profile.feature
@@ -0,0 +1,6 @@
+Feature: Get Profile
+
+ Scenario: Successfully retrieve profile
+ Given A profile is already created for user "GlobalUser"
+ When I send an authenticated GET request to "/profile" to retrieve profile
+ Then I should receive a 200 response for retrieving profile
\ No newline at end of file
diff --git a/src/test/kotlin/resources/feature/list_items.feature b/src/test/kotlin/resources/feature/list_items.feature
new file mode 100644
index 0000000..8bb96a2
--- /dev/null
+++ b/src/test/kotlin/resources/feature/list_items.feature
@@ -0,0 +1,4 @@
+Feature: List Items
+ Scenario: Successfully retrieve items
+ When I send a GET request to "/listItems"
+ Then I should receive a 200 response for listing items
\ No newline at end of file
diff --git a/src/test/kotlin/resources/feature/list_orders.feature b/src/test/kotlin/resources/feature/list_orders.feature
new file mode 100644
index 0000000..866542e
--- /dev/null
+++ b/src/test/kotlin/resources/feature/list_orders.feature
@@ -0,0 +1,5 @@
+Feature: List Orders
+
+ Scenario: Successfully retrieve my orders
+ When I request my orders from "/orders/v1"
+ Then I should receive a 200 response with my orders
\ No newline at end of file
diff --git a/src/test/kotlin/resources/feature/list_users.feature b/src/test/kotlin/resources/feature/list_users.feature
new file mode 100644
index 0000000..860d85a
--- /dev/null
+++ b/src/test/kotlin/resources/feature/list_users.feature
@@ -0,0 +1,5 @@
+Feature: List Users with Authentication
+
+ Scenario: Successfully list users using Global JWT Token
+ When I send an authenticated GET request to "/users/v1/list"
+ Then I should receive a 200 response for listing users
\ No newline at end of file
diff --git a/src/test/kotlin/resources/feature/menu.feature b/src/test/kotlin/resources/feature/menu.feature
new file mode 100644
index 0000000..3d8f488
--- /dev/null
+++ b/src/test/kotlin/resources/feature/menu.feature
@@ -0,0 +1,5 @@
+Feature: Menu Retrieval
+
+ Scenario: Get all menu items
+ When I request the menu
+ Then I should receive a list of menu items
\ No newline at end of file
diff --git a/src/test/kotlin/resources/feature/submit_item.feature b/src/test/kotlin/resources/feature/submit_item.feature
new file mode 100644
index 0000000..dbbcd1a
--- /dev/null
+++ b/src/test/kotlin/resources/feature/submit_item.feature
@@ -0,0 +1,5 @@
+Feature: Submit Item
+ Scenario: Successfully submit a new item
+ Given I have an item with name "Burger", quantity 2, note "No onions", and price 3.5
+ When I submit an item to endpoint "/submitItems"
+ Then I should receive a 200 response for submitting item
\ No newline at end of file
diff --git a/src/test/kotlin/resources/feature/submit_order.feature b/src/test/kotlin/resources/feature/submit_order.feature
new file mode 100644
index 0000000..ccb71e1
--- /dev/null
+++ b/src/test/kotlin/resources/feature/submit_order.feature
@@ -0,0 +1,5 @@
+Feature: Submit Order
+
+ Scenario: Successfully submit an order with item IDs
+ When I submit an order with itemIds to "/orders/v1"
+ Then I should receive a 200 response for order submission
\ No newline at end of file
diff --git a/src/test/kotlin/resources/feature/submit_profile.feature b/src/test/kotlin/resources/feature/submit_profile.feature
new file mode 100644
index 0000000..93d435f
--- /dev/null
+++ b/src/test/kotlin/resources/feature/submit_profile.feature
@@ -0,0 +1,6 @@
+Feature: Submit Profile
+
+ Scenario: Successfully submit a profile
+ Given I have a profile with first name "John", last name "Doe", and phone number "12345678"
+ When I submit the profile to "/profile"
+ Then I should receive a 200 response for submitting profile
\ No newline at end of file
diff --git a/src/test/kotlin/resources/feature/user_register.feature b/src/test/kotlin/resources/feature/user_register.feature
new file mode 100644
index 0000000..8508fc1
--- /dev/null
+++ b/src/test/kotlin/resources/feature/user_register.feature
@@ -0,0 +1,10 @@
+Feature: User Registration
+
+ Scenario: Successful user registration
+ Given I have a user with name "Ali", age 23, username "Ali123", and password "password123"
+ When I send a POST request to "/users/v1/register"
+ Then I should receive a 200 response
+
+# Scenario: List all users
+# When I send a GET request to "/users/v1/list"
+# Then I should receive a 200 response
\ No newline at end of file
diff --git a/swaggerJson/online-ordering-swagger-01.json b/swaggerJson/online-ordering-swagger-01.json
new file mode 100644
index 0000000..3fd7b00
--- /dev/null
+++ b/swaggerJson/online-ordering-swagger-01.json
@@ -0,0 +1 @@
+{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"paths":{"/users/v1/register":{"post":{"tags":["users-controller"],"operationId":"registerUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/submitItems":{"post":{"tags":["items-controller"],"operationId":"submitItem","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitItemRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ItemEntity"}}}}}}},"/orders/v1":{"get":{"tags":["order-controller"],"operationId":"listMyOrders","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Order"}}}}}}},"post":{"tags":["order-controller"],"operationId":"submitOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/auth/login":{"post":{"tags":["authentication-controller"],"operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}}}}},"/users/v1/list":{"get":{"tags":["users-controller"],"operationId":"users","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserRequest"}}}}}}}},"/menus/v1/menu":{"get":{"tags":["menu-controller"],"operationId":"getMenu","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}}},"/listItems":{"get":{"tags":["items-controller"],"operationId":"listItems","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Item"}}}}}}}}},"components":{"schemas":{"UserRequest":{"required":["age","name","password","username"],"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer","format":"int32"},"username":{"type":"string"},"password":{"type":"string"}}},"SubmitItemRequest":{"required":["name","price","quantity"],"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"}}},"ItemEntity":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"},"order":{"$ref":"#/components/schemas/OrderEntity"}}},"OrderEntity":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user":{"$ref":"#/components/schemas/UserEntity"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemEntity"}}}},"UserEntity":{"required":["age","name","password","username"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"age":{"type":"integer","format":"int32"},"username":{"type":"string"},"password":{"type":"string"}}},"SubmitOrderRequest":{"required":["itemIds"],"type":"object","properties":{"itemIds":{"type":"array","items":{"type":"integer","format":"int64"}}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}},"Item":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"order_id":{"type":"integer","format":"int64"},"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"}}},"Order":{"required":["items"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user_id":{"type":"integer","format":"int64"},"items":{"type":"array","items":{"$ref":"#/components/schemas/Item"}}}},"MenuEntity":{"required":["description","name","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string"},"price":{"type":"number","format":"double"}}}}}}
\ No newline at end of file
diff --git a/swaggerJson/online-ordering-swagger-02.json b/swaggerJson/online-ordering-swagger-02.json
new file mode 100644
index 0000000..8fadecd
--- /dev/null
+++ b/swaggerJson/online-ordering-swagger-02.json
@@ -0,0 +1 @@
+{"openapi":"3.0.1","info":{"title":"OpenAPI definition","version":"v0"},"servers":[{"url":"http://localhost:8080","description":"Generated server url"}],"tags":[{"name":"AUTHENTICATION","description":"Endpoints related to user login and token generation"}],"paths":{"/users/v1/register":{"post":{"tags":["users-controller"],"operationId":"registerUser","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/submitItems":{"post":{"tags":["items-controller"],"operationId":"submitItem","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitItemRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"$ref":"#/components/schemas/ItemEntity"}}}}}}},"/profile":{"get":{"tags":["profile-controller"],"operationId":"getProfile","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}},"post":{"tags":["profile-controller"],"operationId":"submitProfile","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"object"}}}}}}},"/orders/v1":{"get":{"tags":["order-controller"],"operationId":"listMyOrders","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Order"}}}}}}},"post":{"tags":["order-controller"],"operationId":"submitOrder","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SubmitOrderRequest"}}},"required":true},"responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}},"/auth/login":{"post":{"tags":["AUTHENTICATION"],"summary":"Login and generate JWT token","description":"Accepts username and password and returns a JWT token upon successful authentication","operationId":"login","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AuthenticationRequest"}}},"required":true},"responses":{"200":{"description":"Successfully authenticated","content":{"*/*":{"schema":{"$ref":"#/components/schemas/AuthenticationResponse"}}}},"400":{"description":"Invalid username or password"}}}},"/users/v1/list":{"get":{"tags":["users-controller"],"operationId":"users","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserRequest"}}}}}}}},"/menu/v1/list":{"get":{"tags":["menu-controller"],"operationId":"getMenu","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/MenuEntity"}}}}}}}},"/listItems":{"get":{"tags":["items-controller"],"operationId":"listItems","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/Item"}}}}}}}},"/hello":{"get":{"tags":["welcome-controller"],"operationId":"sayHello","responses":{"200":{"description":"OK","content":{"*/*":{"schema":{"type":"string"}}}}}}}},"components":{"schemas":{"UserRequest":{"required":["age","name","password","username"],"type":"object","properties":{"name":{"type":"string"},"age":{"type":"integer","format":"int32"},"username":{"type":"string"},"password":{"type":"string"}}},"SubmitItemRequest":{"required":["name","price","quantity"],"type":"object","properties":{"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"}}},"ItemEntity":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"},"order":{"$ref":"#/components/schemas/OrderEntity"}}},"OrderEntity":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user":{"$ref":"#/components/schemas/UserEntity"},"items":{"type":"array","items":{"$ref":"#/components/schemas/ItemEntity"}}}},"UserEntity":{"required":["age","name","password","username"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"age":{"type":"integer","format":"int32"},"username":{"type":"string"},"password":{"type":"string"}}},"ProfileRequest":{"required":["firstName","lastName","phoneNumber"],"type":"object","properties":{"firstName":{"type":"string"},"lastName":{"type":"string"},"phoneNumber":{"type":"string"}}},"SubmitOrderRequest":{"required":["itemIds"],"type":"object","properties":{"itemIds":{"type":"array","items":{"type":"integer","format":"int64"}}}},"AuthenticationRequest":{"required":["password","username"],"type":"object","properties":{"username":{"type":"string"},"password":{"type":"string"}}},"AuthenticationResponse":{"required":["token"],"type":"object","properties":{"token":{"type":"string"}}},"Item":{"type":"object","properties":{"id":{"type":"integer","format":"int64"},"order_id":{"type":"integer","format":"int64"},"name":{"type":"string"},"quantity":{"type":"integer","format":"int64"},"note":{"type":"string"},"price":{"type":"number","format":"double"}}},"Order":{"required":["items"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"user_id":{"type":"integer","format":"int64"},"items":{"type":"array","items":{"$ref":"#/components/schemas/Item"}}}},"MenuEntity":{"required":["description","name","price"],"type":"object","properties":{"id":{"type":"integer","format":"int64"},"name":{"type":"string"},"description":{"type":"string"},"price":{"type":"number","format":"double"}}}}}}
\ No newline at end of file
diff --git a/welcome/pom.xml b/welcome/pom.xml
new file mode 100644
index 0000000..ffb9749
--- /dev/null
+++ b/welcome/pom.xml
@@ -0,0 +1,24 @@
+
+
+ 4.0.0
+
+ com.coded.spring
+ Ordering
+ 0.0.1-SNAPSHOT
+
+
+ welcome
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.10.0
+ test
+
+
+
+
\ No newline at end of file
diff --git a/welcome/src/main/kotlin/helloworld/HelloWorldController.kt b/welcome/src/main/kotlin/helloworld/HelloWorldController.kt
new file mode 100644
index 0000000..97c1651
--- /dev/null
+++ b/welcome/src/main/kotlin/helloworld/HelloWorldController.kt
@@ -0,0 +1,32 @@
+package helloworld
+
+import io.swagger.v3.oas.annotations.Operation
+import io.swagger.v3.oas.annotations.responses.ApiResponse
+import io.swagger.v3.oas.annotations.responses.ApiResponses
+import io.swagger.v3.oas.annotations.tags.Tag
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.web.bind.annotation.GetMapping
+import org.springframework.web.bind.annotation.RestController
+
+@RestController
+@Tag(name = "HOME_PAGE", description = "General welcome and homepage information")
+class WelcomeController(
+ @Value("\${company.name}") val companyName: String,
+ @Value("\${festive-mode:false}") val festiveMode: Boolean
+) {
+
+ @GetMapping("/hello")
+ @Operation(summary = "Display welcome message", description = "Returns a greeting message based on festive mode")
+ @ApiResponses(
+ value = [
+ ApiResponse(responseCode = "200", description = "Welcome message returned successfully")
+ ]
+ )
+ fun sayHello(): String {
+ return if (festiveMode) {
+ "🎉 Eidkom Mubarak from $companyName!"
+ } else {
+ "Welcome to Online Ordering by $companyName"
+ }
+ }
+}
\ No newline at end of file
diff --git a/welcome/src/main/resources/application.properties b/welcome/src/main/resources/application.properties
new file mode 100644
index 0000000..d588ae9
--- /dev/null
+++ b/welcome/src/main/resources/application.properties
@@ -0,0 +1,9 @@
+spring.application.name=Kotlin.SpringbootV2
+server.port = 8083
+
+spring.datasource.url=jdbc:postgresql://localhost:5432/myHelloDatabase
+spring.datasource.username=postgres
+spring.datasource.password=yosaka
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+
+springdoc.api-docs.path=/api-docs