Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .DS_Store
Binary file not shown.
102 changes: 102 additions & 0 deletions SOLUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
Solution for Billie Pair Programming Exercise
=============
### Requirements:

* Issuing an invoice for the buyer once the merchant notifies us the goods have been shipped

### Steps to test
1- open the swagger ui:
http://localhost:8080/swagger-ui/index.html

2- find the list of available merchants or add a new one:

```
[
{
"id": "59267c88-a561-4e07-ab5d-75b64bed6a7c",
"name": "amazon.com"
},
{
"id": "a177028a-fcde-4b56-95aa-08c0fa9fca2e",
"name": "ebay.com"
}
]
```

3- find the list of available customers or create a new one:
```
[
{
"id": "25fca542-cb2f-4c5d-81e1-cbe2f4a6fd83",
"name": "John Smith",
"address_details": "5703 Louetta Rd, Texas"
},
{
"id": "2867689a-f3b1-45ed-a1ad-7adf3b40a0ab",
"name": "Josh Long",
"address_details": "9069 Holman Rd NW, Seattle"
},
{
"id": "1325673e-ea2e-4b0b-822c-051204ecc7e8",
"name": "Esfandiyar Talebi",
"address_details": "Yavux su, burhabine, Istanbul"
}
]
```

4- find the list of available Products or add new ones:
```
[
{
"id": "126ff1c1-1e28-4355-a5d2-1aab3f3c48b8",
"name": "iPhone 15 SE",
"price": 999
},
{
"id": "93e77e2e-30ea-40d2-b505-b46c00f15680",
"name": "iPhone 15 Max",
"price": 1299
}
]
```
5- Create an Order for one of the Customers with available product uid:
```
{
"amount": 100.00,
"customerId": "25fca542-cb2f-4c5d-81e1-cbe2f4a6fd83"
}
```
and note down the order uid:
```
{
"id": "94449231-63de-441a-bb9b-d70e802ab957"
}
```
6- Notify the shipment of the order by using merchant resource

```
{
"shipmentUId": "94449231-63de-441a-bb9b-d70e802ab955",
"orderUId": "94449231-63de-441a-bb9b-d70e802ab957",
"customerUId": "25fca542-cb2f-4c5d-81e1-cbe2f4a6fd83"
}
```
as a result, invoice will be generated for the customer.

7- find the newly created Invoice within the list of invoices

### work in progress
1- Handling the deduplication of shipment event
2- Adding more integration tests for DAO classes


#### Prerequisites

Running the tests:
```shell
cd <project_root>
docker compose up database -d
gradle flywayMigrate
gradle clean build
docs at -> http://localhost:8080/swagger-ui/index.html
```
13 changes: 13 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ repositories {
mavenCentral()
}

extra["testcontainersVersion"] = "1.18.3"

dependencies {
implementation("org.springframework.boot:spring-boot-starter-validation")
implementation("org.springframework.boot:spring-boot-starter-data-jdbc")
Expand All @@ -39,10 +41,21 @@ dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
runtimeOnly("org.postgresql:postgresql")
implementation ("org.flywaydb:flyway-core")

// adding test container support
testImplementation("org.testcontainers:junit-jupiter")
testImplementation("org.testcontainers:postgresql")

testImplementation("org.springframework.boot:spring-boot-starter-test")
}

dependencyManagement {
imports {
mavenBom("org.testcontainers:testcontainers-bom:${property("testcontainersVersion")}")
}
}

flyway {
url = dbConf.getProperty("DATABASE_URL")
user = dbConf.getProperty("POSTGRES_USER")
Expand Down
18 changes: 13 additions & 5 deletions src/main/kotlin/io/billie/countries/data/CountryRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,21 @@ class CountryRepository {
@Autowired
lateinit var jdbcTemplate: JdbcTemplate

@Transactional(readOnly=true)
@Transactional(readOnly = true)
fun findCountries(): List<CountryResponse> {
val query = jdbcTemplate.query(
"select id, name, country_code from organisations_schema.countries",
countryResponseMapper()
return jdbcTemplate.query(
"select id, name, country_code from organisations_schema.countries",
countryResponseMapper()
)
return query
}

@Transactional
fun findCountryByCode (countryCode: String): Optional<CountryResponse>{
return jdbcTemplate.query(
"select id, name, country_code from organisations_schema.countries where country_code=?",
countryResponseMapper(),
countryCode
).stream().findFirst()
}

private fun countryResponseMapper() = RowMapper<CountryResponse> { it: ResultSet, _: Int ->
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.billie.countries.exception

class CountryNotFoundException (message: String): RuntimeException(message) {
}
4 changes: 4 additions & 0 deletions src/main/kotlin/io/billie/countries/service/CountryService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package io.billie.countries.service

import io.billie.countries.data.CityRepository
import io.billie.countries.data.CountryRepository
import io.billie.countries.exception.CountryNotFoundException
import io.billie.countries.model.CityResponse
import io.billie.countries.model.CountryResponse
import org.springframework.stereotype.Service
Expand All @@ -14,4 +15,7 @@ class CountryService(val dbCountry: CountryRepository, val dbCity: CityRepositor
}
fun findCities(countryCode: String): List<CityResponse> = dbCity.findByCountryCode(countryCode)

fun findCountryByCode (countryCode: String): CountryResponse {
return dbCountry.findCountryByCode(countryCode).orElseThrow { CountryNotFoundException("country with code: $countryCode was not found!") }
}
}
65 changes: 65 additions & 0 deletions src/main/kotlin/io/billie/customers/data/CustomerRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.billie.customers.data

import io.billie.customers.dto.CustomerRequest
import io.billie.customers.model.Customer
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.RowMapper
import org.springframework.jdbc.support.GeneratedKeyHolder
import org.springframework.jdbc.support.KeyHolder
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional
import java.sql.ResultSet
import java.util.*

@Repository
class CustomerRepository {
@Autowired
lateinit var jdbcTemplate: JdbcTemplate

@Transactional(readOnly=true)
fun findCustomerById(id: UUID): Optional<Customer> {
return jdbcTemplate.query(
"select id, name, address_details from organisations_schema.customers where id= ?",
customerResponseMapper(),
id
).stream().findFirst();
}

private fun customerResponseMapper() = RowMapper<Customer> { it: ResultSet, _: Int ->
Customer(
it.getObject("id", UUID::class.java),
it.getString("name"),
it.getString("address_details")
)
}

@Transactional
fun findCustomers(): List<Customer> {
return jdbcTemplate.query(
"select id, name, address_details from organisations_schema.customers",
customerResponseMapper(),
)
}

@Transactional
fun createCustomer(customerRequest: CustomerRequest): UUID {
val keyHolder: KeyHolder = GeneratedKeyHolder()
jdbcTemplate.update(
{ connection ->
val ps = connection.prepareStatement(
"""
INSERT INTO organisations_schema.customers
(name, address_details)
VALUES (?, ?)
""".trimIndent(),
arrayOf("id")
)
ps.setString(1, customerRequest.name)
ps.setString(2, customerRequest.address)
ps
}, keyHolder
)
return keyHolder.getKeyAs(UUID::class.java)!!
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/io/billie/customers/dto/CustomerRequest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.billie.customers.dto

import com.fasterxml.jackson.annotation.JsonProperty
import javax.validation.constraints.NotBlank

data class CustomerRequest (
@field:NotBlank val name: String,
@field:NotBlank @JsonProperty("address_details")
val address: String
)
14 changes: 14 additions & 0 deletions src/main/kotlin/io/billie/customers/dto/CustomerResponse.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.billie.customers.dto

import com.fasterxml.jackson.annotation.JsonProperty
import java.util.UUID
import javax.validation.constraints.NotBlank

data class CustomerResponse (
val id: UUID,

@field:NotBlank val name: String,

@field:NotBlank @JsonProperty("address_details")
val address: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package io.billie.customers.exception

class CustomerNotFoundException (message: String): RuntimeException (message) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.billie.customers.mapper

import io.billie.customers.dto.CustomerResponse
import io.billie.customers.model.Customer


fun Customer.toCustomerResponse(): CustomerResponse =
CustomerResponse(
id = this.id,
name = this.name,
address = this.address
)
14 changes: 14 additions & 0 deletions src/main/kotlin/io/billie/customers/model/Customer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.billie.customers.model

import com.fasterxml.jackson.annotation.JsonProperty
import java.util.UUID
import javax.validation.constraints.NotBlank

data class Customer (
val id: UUID,

@field:NotBlank val name: String,

// TODO: shall be replaced with AddressDetail later
@field:NotBlank @JsonProperty("address_details") val address: String
)
65 changes: 65 additions & 0 deletions src/main/kotlin/io/billie/customers/resource/CustomerResource.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package io.billie.customers.resource

import io.billie.customers.dto.CustomerRequest
import io.billie.customers.dto.CustomerResponse
import io.billie.customers.mapper.toCustomerResponse
import io.billie.customers.service.CustomerService
import io.billie.orders.dto.OrderResponse
import io.billie.organisations.model.Entity
import io.swagger.v3.oas.annotations.media.ArraySchema
import io.swagger.v3.oas.annotations.media.Content
import io.swagger.v3.oas.annotations.media.Schema
import io.swagger.v3.oas.annotations.responses.ApiResponse
import io.swagger.v3.oas.annotations.responses.ApiResponses
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import kotlin.streams.toList

@RestController
@RequestMapping ("/customers")
class CustomerResource (val customerService: CustomerService) {
@GetMapping
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "returns all customers ",
content = [
(Content(
mediaType = "application/json",
array = (ArraySchema(schema = Schema(implementation = CustomerResponse::class)))
))]
)
]
)
fun findCustomers (): ResponseEntity<List<CustomerResponse>>{
return ResponseEntity.ok(customerService.findCustomers()
.stream().map { it.toCustomerResponse() }
.toList())
}

@PostMapping
@ApiResponses(
value = [
ApiResponse(
responseCode = "201",
description = "Accepted the new customer",
content = [
(Content(
mediaType = "application/json",
array = (ArraySchema(schema = Schema(implementation = Entity::class)))
))]
),
ApiResponse(responseCode = "400", description = "Bad request", content = [Content()])]
)
fun createCustomer(@RequestBody customerRequest: CustomerRequest): ResponseEntity<Entity>{
val customerId = customerService.createCustomer(customerRequest)

return ResponseEntity.status(HttpStatus.CREATED).body(Entity(customerId))
}
}
11 changes: 11 additions & 0 deletions src/main/kotlin/io/billie/customers/service/CustomerService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.billie.customers.service

import io.billie.customers.dto.CustomerRequest
import io.billie.customers.model.Customer
import java.util.UUID

interface CustomerService {
fun findCustomers(): List<Customer>
fun findCustomerByUid (uid: UUID): Customer
fun createCustomer(customerRequest: CustomerRequest): UUID
}
Loading