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
2 changes: 1 addition & 1 deletion database.env
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
DATABASE_DRIVER=org.postgresql.Driver
POSTGRES_USER=postgres
POSTGRES_PASSWORD=7gn[[G780PH[,JP'HIUG
POSTGRES_PASSWORD=postgres
DATABASE_NAME=organisations
DATABASE_HOSTNAME=database
DATABASE_URL=jdbc:postgresql://localhost:5432/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,25 @@ class OrganisationRepository {
if(!valuesValid(organisation)) {
throw UnableToFindCountry(organisation.countryCode)
}
val id: UUID = createContactDetails(organisation.contactDetails)
return createOrganisation(organisation, id)
val contactDetailsId = createContactDetails(organisation.contactDetails)
val addressId: UUID = createAddress(organisation.address)
return createOrganisation(organisation, contactDetailsId, addressId)
}

private fun valuesValid(organisation: OrganisationRequest): Boolean {
val reply: Int? = jdbcTemplate.query(
"select count(country_code) from organisations_schema.countries c WHERE c.country_code = ?",
"select count(country_code) from organisations_schema.cities c WHERE c.country_code = ? AND c.name = ?",
ResultSetExtractor {
it.next()
it.getInt(1)
},
organisation.countryCode
organisation.countryCode,
organisation.address.city
)
return (reply != null) && (reply > 0)
}

private fun createOrganisation(org: OrganisationRequest, contactDetailsId: UUID): UUID {
private fun createOrganisation(org: OrganisationRequest, contactDetailsId: UUID, addressId: UUID): UUID {
val keyHolder: KeyHolder = GeneratedKeyHolder()
jdbcTemplate.update(
{ connection ->
Expand All @@ -59,8 +61,9 @@ class OrganisationRepository {
"vat_number, " +
"registration_number, " +
"legal_entity_type, " +
"contact_details_id" +
") VALUES (?, ?, ?, ?, ?, ?, ?)",
"contact_details_id," +
"address_id" +
") VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
arrayOf("id")
)
ps.setString(1, org.name)
Expand All @@ -70,12 +73,35 @@ class OrganisationRepository {
ps.setString(5, org.registrationNumber)
ps.setString(6, org.legalEntityType.toString())
ps.setObject(7, contactDetailsId)
ps.setObject(8, addressId)
ps
}, keyHolder
)
return keyHolder.getKeyAs(UUID::class.java)!!
}

private fun createAddress(address: Address): UUID {
val keyHolder: KeyHolder = GeneratedKeyHolder()
jdbcTemplate.update(
{ connection ->
val ps = connection.prepareStatement(
"insert into organisations_schema.addresses " +
"(" +
"city, " +
"street_and_number, " +
"postal_code" +
") values(?,?,?)",
arrayOf("id")
)
ps.setString(1, address.city)
ps.setString(2, address.streetAndNumber)
ps.setString(3, address.postalCode)
ps
},
keyHolder
)
return keyHolder.getKeyAs(UUID::class.java)!!
}
private fun createContactDetails(contactDetails: ContactDetailsRequest): UUID {
val keyHolder: KeyHolder = GeneratedKeyHolder()
jdbcTemplate.update(
Expand Down Expand Up @@ -112,11 +138,16 @@ class OrganisationRepository {
"o.contact_details_id as contact_details_id, " +
"cd.phone_number as phone_number, " +
"cd.fax as fax, " +
"cd.email as email " +
"cd.email as email, " +
"o.address_id as address_id, " +
"a.postal_code as postal_code, " +
"a.street_and_number as street_and_number, " +
"a.city as city " +
"from " +
"organisations_schema.organisations o " +
"INNER JOIN organisations_schema.contact_details cd on o.contact_details_id::uuid = cd.id::uuid " +
"INNER JOIN organisations_schema.countries c on o.country_code = c.country_code "
"INNER JOIN organisations_schema.countries c on o.country_code = c.country_code " +
"INNER JOIN organisations_schema.addresses a on o.address_id::uuid = a.id::uuid "

private fun organisationMapper() = RowMapper<OrganisationResponse> { it: ResultSet, _: Int ->
OrganisationResponse(
Expand All @@ -127,10 +158,17 @@ class OrganisationRepository {
it.getString("vat_number"),
it.getString("registration_number"),
LegalEntityType.valueOf(it.getString("legal_entity_type")),
mapContactDetails(it)
mapContactDetails(it),
mapAddress(it)
)
}

private fun mapAddress(it: ResultSet) = Address(
UUID.fromString(it.getString("address_id")),
it.getString("city"),
it.getString("postal_code"),
it.getString("street_and_number")
)
private fun mapContactDetails(it: ResultSet): ContactDetails {
return ContactDetails(
UUID.fromString(it.getString("contact_details_id")),
Expand Down
12 changes: 12 additions & 0 deletions src/main/kotlin/io/billie/organisations/viewmodel/Address.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package io.billie.organisations.viewmodel

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

data class Address(
val id: UUID?,
@field:NotBlank val city: String,
@field:NotBlank @JsonProperty("postal_code") val postalCode: String,
@field:NotBlank @JsonProperty("street_and_number") val streetAndNumber: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import com.fasterxml.jackson.annotation.JsonFormat
import com.fasterxml.jackson.annotation.JsonProperty
import org.springframework.data.relational.core.mapping.Table
import java.time.LocalDate
import java.util.*
import javax.validation.constraints.NotBlank

@Table("ORGANISATIONS")
Expand All @@ -16,4 +15,5 @@ data class OrganisationRequest(
@JsonProperty("registration_number") val registrationNumber: String?,
@JsonProperty("legal_entity_type") val legalEntityType: LegalEntityType,
@JsonProperty("contact_details") val contactDetails: ContactDetailsRequest,
@JsonProperty("address") val address: Address,
)
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ data class OrganisationResponse(
@JsonProperty("registration_number") val registrationNumber: String?,
@JsonProperty("legal_entity_type") val legalEntityType: LegalEntityType,
@JsonProperty("contact_details") val contactDetails: ContactDetails,
@JsonProperty("address") val address: Address
)
10 changes: 10 additions & 0 deletions src/main/resources/db/migration/V9__add_addresses_table.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS organisations_schema.addresses
(
id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
city VARCHAR(100),
street_and_number VARCHAR(100),
postal_code VARCHAR(20)
);

ALTER TABLE organisations_schema.organisations ADD COLUMN address_id VARCHAR(36);

Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
package io.billie.functional

import com.fasterxml.jackson.databind.ObjectMapper
import io.billie.functional.data.Fixtures.bbcAddressFixture
import io.billie.functional.data.Fixtures.bbcContactFixture
import io.billie.functional.data.Fixtures.bbcFixture
import io.billie.functional.data.Fixtures.orgRequestJson
import io.billie.functional.data.Fixtures.orgRequestJsonAddressCityBlank
import io.billie.functional.data.Fixtures.orgRequestJsonAddressCityIncorrect
import io.billie.functional.data.Fixtures.orgRequestJsonAddressCityNotBelongingToCountry
import io.billie.functional.data.Fixtures.orgRequestJsonCountryCodeBlank
import io.billie.functional.data.Fixtures.orgRequestJsonCountryCodeIncorrect
import io.billie.functional.data.Fixtures.orgRequestJsonNoName
import io.billie.functional.data.Fixtures.orgRequestJsonNameBlank
import io.billie.functional.data.Fixtures.orgRequestJsonNoAddress
import io.billie.functional.data.Fixtures.orgRequestJsonNoAddressPostalCode
import io.billie.functional.data.Fixtures.orgRequestJsonNoAddressStreet
import io.billie.functional.data.Fixtures.orgRequestJsonNoContactDetails
import io.billie.functional.data.Fixtures.orgRequestJsonNoCountryCode
import io.billie.functional.data.Fixtures.orgRequestJsonNoLegalEntityType
import io.billie.functional.data.Fixtures.orgResponseJson
import io.billie.organisations.viewmodel.Entity
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.core.IsEqual.equalTo
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
Expand All @@ -25,12 +38,16 @@ import org.springframework.jdbc.core.JdbcTemplate
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultHandlers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.util.*
import java.util.stream.Stream


@AutoConfigureMockMvc
@SpringBootTest(webEnvironment = DEFINED_PORT)
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class CanStoreAndReadOrganisationTest {

@LocalServerPort
Expand All @@ -44,7 +61,12 @@ class CanStoreAndReadOrganisationTest {

@Autowired
private lateinit var template: JdbcTemplate

@BeforeEach
fun cleanOrgs() {
template.execute("delete from organisations_schema.organisations")
template.execute("delete from organisations_schema.contact_details")
template.execute("delete from organisations_schema.addresses")
}
@Test
fun orgs() {
mockMvc.perform(
Expand All @@ -71,47 +93,44 @@ class CanStoreAndReadOrganisationTest {
}

@Test
fun cannotStoreOrgWhenCountryCodeIsMissing() {
fun cannotStoreOrgWhenNoLegalEntityType() {
mockMvc.perform(
post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJsonNoCountryCode())
post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJsonNoLegalEntityType())
)
.andExpect(status().isBadRequest)
}

@Test
fun cannotStoreOrgWhenCountryCodeIsBlank() {
fun cannotStoreOrgWhenNoContactDetails() {
mockMvc.perform(
post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJsonCountryCodeBlank())
post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJsonNoContactDetails())
)
.andExpect(status().isBadRequest)
}

@Test
fun cannotStoreOrgWhenCountryCodeIsNotRecognised() {
@ParameterizedTest
@MethodSource("invalidAddressPayloads")
fun cannotStoreOrgWhenInvalidAddressInput(invalidPayload: String) {
mockMvc.perform(
post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJsonCountryCodeIncorrect())
post("/organisations").contentType(APPLICATION_JSON).content(invalidPayload)
)
.andExpect(status().isBadRequest)
}

@Test
fun cannotStoreOrgWhenNoLegalEntityType() {
mockMvc.perform(
post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJsonNoLegalEntityType())
)
.andExpect(status().isBadRequest)
}
fun invalidAddressPayloads(): Stream<Arguments> = Stream.of(
Arguments.of(orgRequestJsonNoAddress()),
Arguments.of(orgRequestJsonCountryCodeBlank()),
Arguments.of(orgRequestJsonCountryCodeIncorrect()),
Arguments.of(orgRequestJsonNoCountryCode()),
Arguments.of(orgRequestJsonNoAddressStreet()),
Arguments.of(orgRequestJsonNoAddressPostalCode()),
Arguments.of(orgRequestJsonAddressCityIncorrect()),
Arguments.of(orgRequestJsonAddressCityNotBelongingToCountry()),
Arguments.of(orgRequestJsonAddressCityBlank()),
)

@Test
fun cannotStoreOrgWhenNoContactDetails() {
mockMvc.perform(
post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJsonNoContactDetails())
)
.andExpect(status().isBadRequest)
}

@Test
fun canStoreOrg() {
fun canAndRetrieveStoreOrg() {
val result = mockMvc.perform(
post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJson())
)
Expand All @@ -126,6 +145,29 @@ class CanStoreAndReadOrganisationTest {
val contactDetailsId: UUID = UUID.fromString(org["contact_details_id"] as String)
val contactDetails: Map<String, Any> = contactDetailsFromDatabase(contactDetailsId)
assertDataMatches(contactDetails, bbcContactFixture(contactDetailsId))

val addressId: UUID = UUID.fromString(org["address_id"] as String)
val address = addressFromDatabase(addressId)
assertDataMatches(address, bbcAddressFixture(addressId))

val countryId = countryIdFromDatabase(org["country_code"] as String)

mockMvc.perform(
get("/organisations")
.contentType(APPLICATION_JSON)
).andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk())
.andExpectAll(
MockMvcResultMatchers.content()
.json(
orgResponseJson(
response.id.toString(),
countryId,
contactDetailsId.toString(),
addressId.toString()
)
)
)
}

fun assertDataMatches(reply: Map<String, Any>, assertions: Map<String, Any>) {
Expand All @@ -140,7 +182,13 @@ class CanStoreAndReadOrganisationTest {
private fun orgFromDatabase(id: UUID): MutableMap<String, Any> =
queryEntityFromDatabase("select * from organisations_schema.organisations where id = ?", id)

private fun countryIdFromDatabase(countryCode: String): String =
template.queryForObject("select id from organisations_schema.countries where country_code = ?", String::class.java, countryCode)

private fun contactDetailsFromDatabase(id: UUID): MutableMap<String, Any> =
queryEntityFromDatabase("select * from organisations_schema.contact_details where id = ?", id)

private fun addressFromDatabase(id: UUID): Map<String, Any> =
queryEntityFromDatabase("select * from organisations_schema.addresses where id = ?", id)

}
Loading