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
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,14 @@ docs at -> http://localhost:8080/swagger-ui/index.html
Work has been started but not done yet to containerise the kotlin service.

The service runs in the host right now. Feel free to fix that if it makes your life easier


### From candidate:

Implemented features:
1. Add an address to an organisation

Additional improvements:
1. Integration tests now run on random ports
2. Flyway migration runs on application start
3. Now, it is possible to run service inside docker on local
5 changes: 3 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import java.io.File

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import java.io.FileInputStream
import java.util.*
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("org.flywaydb.flyway") version "9.3.1"
Expand Down Expand Up @@ -38,6 +38,7 @@ dependencies {
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
implementation("org.flywaydb:flyway-core:6.4.1")
runtimeOnly("org.postgresql:postgresql")

testImplementation("org.springframework.boot:spring-boot-starter-test")
Expand Down
8 changes: 6 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ services:
command:
- "--providers.docker=true"
- "--providers.docker.exposedByDefault=false"
- "--entryPoints.web.address=:80"
- "--entryPoints.web.address=:8080"
- "--entryPoints.websecure.address=:443"
- "--certificatesResolvers.organisationsresolver.acme.email=team@organisations.manager"
- "--certificatesResolvers.organisationsresolver.acme.storage=acme.json"
Expand Down Expand Up @@ -36,7 +36,11 @@ services:
env_file:
- database.env
- service.env
environment:
- DATABASE_URL=jdbc:postgresql://host.docker.internal:5432/
ports:
- "8080:8080"
labels:
- "traefik.enable=true"
- "traefik.http.routers.organisations.rule=PathPrefix(`/swagger-ui`) || PathPrefix(`/organisations`) || PathPrefix(`/countries`)"
- "traefik.http.services.organisations.loadbalancer.server.port=80"
- "traefik.http.services.organisations.loadbalancer.server.port=8080"
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.billie.organisations.data

import java.util.*


class CityWithIdDoesNotExist(val cityId: UUID) : RuntimeException()
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
package io.billie.organisations.data

import io.billie.countries.model.CountryResponse
import io.billie.organisations.viewmodel.*
import io.billie.organisations.viewmodel.ContactDetails
import io.billie.organisations.viewmodel.ContactDetailsRequest
import io.billie.organisations.viewmodel.LegalEntityType
import io.billie.organisations.viewmodel.OrganisationAddressRequest
import io.billie.organisations.viewmodel.OrganisationRequest
import io.billie.organisations.viewmodel.OrganisationResponse
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.jdbc.core.ResultSetExtractor
Expand Down Expand Up @@ -35,6 +40,43 @@ class OrganisationRepository {
return createOrganisation(organisation, id)
}

@Transactional
fun addAddress(orgId: UUID, orgAddress: OrganisationAddressRequest): UUID {
validateAddress(orgId, orgAddress)
return createAddress(orgId, orgAddress)
}

private fun createAddress(orgId: UUID, orgAddress: OrganisationAddressRequest): UUID {
val keyHolder: KeyHolder = GeneratedKeyHolder()
jdbcTemplate.update(
{ connection ->
val ps = connection.prepareStatement(
"""
INSERT INTO organisations_schema.addresses (
organisation_id,
city_id,
pin_code,
street_name,
plot_number,
floor,
apartment_number
) VALUES (?, ?, ?, ?, ?, ?, ?)
""".trimIndent(),
arrayOf("id")
)
ps.setObject(1, orgId)
ps.setObject(2, orgAddress.cityId)
ps.setString(3, orgAddress.pinCode)
ps.setString(4, orgAddress.streetName)
ps.setString(5, orgAddress.plotNumber)
ps.setString(6, orgAddress.floor)
ps.setString(7, orgAddress.apartmentNumber)
ps
}, keyHolder
)
return keyHolder.getKeyAs(UUID::class.java)!!
}

private fun valuesValid(organisation: OrganisationRequest): Boolean {
val reply: Int? = jdbcTemplate.query(
"select count(country_code) from organisations_schema.countries c WHERE c.country_code = ?",
Expand All @@ -47,6 +89,36 @@ class OrganisationRepository {
return (reply != null) && (reply > 0)
}

private fun validateAddress(orgId: UUID, orgAddress: OrganisationAddressRequest): Boolean {
val orgExists: Boolean? = jdbcTemplate.query(
"select exists(select 1 from organisations_schema.organisations o WHERE o.id = ?)",
ResultSetExtractor {
it.next()
it.getBoolean(1)
},
orgId
)

if (!orgExists!!)
throw OrganisationWithIdDoesNotExist(orgId)

val cityExists: Boolean? = jdbcTemplate.query(
"select exists(select 1 from organisations_schema.cities c WHERE c.id = ?)",
ResultSetExtractor {
it.next()
it.getBoolean(1)
},
orgAddress.cityId
)

if (!cityExists!!)
throw CityWithIdDoesNotExist(orgAddress.cityId)

// We can also check if both city and organisation are in same country if that's what is required

return true
}

private fun createOrganisation(org: OrganisationRequest, contactDetailsId: UUID): UUID {
val keyHolder: KeyHolder = GeneratedKeyHolder()
jdbcTemplate.update(
Expand Down Expand Up @@ -147,5 +219,4 @@ class OrganisationRepository {
it.getString("country_code")
)
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package io.billie.organisations.data

import java.util.*


class OrganisationWithIdDoesNotExist(val organisationId: UUID) : RuntimeException()
Original file line number Diff line number Diff line change
@@ -1,19 +1,29 @@
package io.billie.organisations.resource

import io.billie.organisations.data.CityWithIdDoesNotExist
import io.billie.organisations.data.OrganisationWithIdDoesNotExist
import io.billie.organisations.data.UnableToFindCountry
import io.billie.organisations.service.OrganisationService
import io.billie.organisations.viewmodel.*
import io.billie.organisations.viewmodel.Entity
import io.billie.organisations.viewmodel.OrganisationAddressRequest
import io.billie.organisations.viewmodel.OrganisationRequest
import io.billie.organisations.viewmodel.OrganisationResponse
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.HttpStatus.BAD_REQUEST
import org.springframework.web.bind.annotation.*
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
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 org.springframework.web.server.ResponseStatusException
import java.util.*
import java.util.UUID
import javax.validation.Valid
import javax.validation.constraints.Size


@RestController
Expand Down Expand Up @@ -46,4 +56,31 @@ class OrganisationResource(val service: OrganisationService) {
}
}

@PostMapping(path = arrayOf("/{org_id}/addresses"))
@ApiResponses(
value = [
ApiResponse(
responseCode = "200",
description = "Accepted the address for organisation",
content = [
(Content(
mediaType = "application/json",
array = (ArraySchema(schema = Schema(implementation = Entity::class)))
))]
),
ApiResponse(responseCode = "400", description = "Bad request", content = [Content()])]
)
fun postAddress(
@Size(min = 36, max = 36) @PathVariable("org_id") orgId: UUID,
@Valid @RequestBody orgAddress: OrganisationAddressRequest
): Entity {
try {
val id = service.addAddressToOrg(orgId, orgAddress)
return Entity(id)
} catch (e: OrganisationWithIdDoesNotExist) {
throw ResponseStatusException(BAD_REQUEST, e.message)
} catch (e: CityWithIdDoesNotExist) {
throw ResponseStatusException(BAD_REQUEST, e.message)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package io.billie.organisations.service

import io.billie.organisations.data.OrganisationRepository
import io.billie.organisations.viewmodel.OrganisationAddressRequest
import io.billie.organisations.viewmodel.OrganisationRequest
import io.billie.organisations.viewmodel.OrganisationResponse
import org.springframework.stereotype.Service
Expand All @@ -15,4 +16,7 @@ class OrganisationService(val db: OrganisationRepository) {
return db.create(organisation)
}

fun addAddressToOrg(orgId: UUID, orgAddress: OrganisationAddressRequest): UUID {
return db.addAddress(orgId, orgAddress)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.billie.organisations.viewmodel

import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.data.relational.core.mapping.Table
import java.util.*
import javax.validation.constraints.NotBlank
import javax.validation.constraints.Size

@Table("ADDRESSES")
data class OrganisationAddressRequest(
@Size(min = 36, max = 36) @JsonProperty("city_id") val cityId: UUID,
@field:NotBlank @JsonProperty("pin_code") val pinCode: String,
@field:NotBlank @JsonProperty("street_name") val streetName: String,
@field:NotBlank @JsonProperty("plot_number") val plotNumber: String,
val floor: String? = null,
@JsonProperty("apartment_number") val apartmentNumber: String? = null,
)
2 changes: 2 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ spring.datasource.username=${POSTGRES_USER}
spring.datasource.password=${POSTGRES_PASSWORD}
spring.datasource.schema=${DATABASE_SCHEMA}
spring.datasource.initialization-mode=always
spring.flyway.enabled=true
spring.flyway.locations=classpath:db/migration
13 changes: 13 additions & 0 deletions src/main/resources/db/migration/V9__addresses_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS organisations_schema.addresses
(
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
organisation_id UUID NOT NULL,
city_id UUID NOT NULL,
pin_code VARCHAR(10) NOT NULL,
street_name VARCHAR(100) NOT NULL,
plot_number VARCHAR(5) NOT NULL,
floor VARCHAR(3) DEFAULT NULL,
apartment_number VARCHAR(4) DEFAULT NULL,
FOREIGN KEY (organisation_id) REFERENCES organisations_schema.organisations(id) ON DELETE CASCADE,
FOREIGN KEY (city_id) REFERENCES organisations_schema.cities(id)
);
12 changes: 2 additions & 10 deletions src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
package io.billie.functional

import io.billie.functional.matcher.IsUUID.isUuid
import org.hamcrest.Description
import org.hamcrest.TypeSafeMatcher
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT
import org.springframework.boot.test.web.server.LocalServerPort
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.http.MediaType.APPLICATION_JSON
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.util.*


@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = DEFINED_PORT)
@SpringBootTest(webEnvironment = RANDOM_PORT)
class CanReadLocationsTest {

@LocalServerPort
private val port = 8080

@Autowired
private lateinit var mockMvc: MockMvc

Expand Down
Loading