diff --git a/database.env b/database.env index 8e52ef5..b03df47 100644 --- a/database.env +++ b/database.env @@ -1,7 +1,7 @@ DATABASE_DRIVER=org.postgresql.Driver POSTGRES_USER=postgres POSTGRES_PASSWORD=7gn[[G780PH[,JP'HIUG -DATABASE_NAME=organisations +DATABASE_NAME=organisations_schema DATABASE_HOSTNAME=database DATABASE_URL=jdbc:postgresql://localhost:5432/ DATABASE_PORT=5432 diff --git a/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt b/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt index 8c0026b..b2ea437 100644 --- a/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt +++ b/src/main/kotlin/io/billie/organisations/data/OrganisationRepository.kt @@ -31,8 +31,10 @@ class OrganisationRepository { if(!valuesValid(organisation)) { throw UnableToFindCountry(organisation.countryCode) } - val id: UUID = createContactDetails(organisation.contactDetails) - return createOrganisation(organisation, id) + val contactId: UUID = createContactDetails(organisation.contactDetails) + val addressId: UUID? = organisation.address?.let { createAddress(it) } + + return createOrganisation(organisation, contactId, addressId) } private fun valuesValid(organisation: OrganisationRequest): Boolean { @@ -47,7 +49,7 @@ class OrganisationRepository { 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 -> @@ -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) @@ -70,6 +73,7 @@ class OrganisationRepository { ps.setString(5, org.registrationNumber) ps.setString(6, org.legalEntityType.toString()) ps.setObject(7, contactDetailsId) + ps.setObject(8, addressId) ps }, keyHolder ) @@ -99,6 +103,29 @@ class OrganisationRepository { return keyHolder.getKeyAs(UUID::class.java)!! } + private fun createAddress(address: AddressRequest): UUID { + val keyHolder: KeyHolder = GeneratedKeyHolder() + jdbcTemplate.update( + { connection -> + val ps = connection.prepareStatement( + "insert into organisations_schema.address " + + "(" + + "street_name, " + + "home_number, " + + "zip_code" + + ") values(?,?,?)", + arrayOf("id") + ) + ps.setString(1, address.street_name) + ps.setString(2, address.home_number) + ps.setString(3, address.zip_code) + ps + }, + keyHolder + ) + return keyHolder.getKeyAs(UUID::class.java)!! + } + private fun organisationQuery() = "select " + "o.id as id, " + "o.name as name, " + @@ -110,13 +137,18 @@ class OrganisationRepository { "o.registration_number as registration_number," + "o.legal_entity_type as legal_entity_type," + "o.contact_details_id as contact_details_id, " + + "o.address_id as address_id, " + "cd.phone_number as phone_number, " + "cd.fax as fax, " + - "cd.email as email " + + "cd.email as email, " + + "ad.street_name as street_name, " + + "ad.home_number as home_number, " + + "ad.zip_code as zip_code " + "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 " + + "LEFT JOIN organisations_schema.address ad ON o.address_id::uuid = ad.id::uuid; " private fun organisationMapper() = RowMapper { it: ResultSet, _: Int -> OrganisationResponse( @@ -127,7 +159,8 @@ class OrganisationRepository { it.getString("vat_number"), it.getString("registration_number"), LegalEntityType.valueOf(it.getString("legal_entity_type")), - mapContactDetails(it) + mapContactDetails(it), + mapAddress(it) ) } @@ -140,6 +173,19 @@ class OrganisationRepository { ) } + private fun mapAddress(it: ResultSet): Address? { + val addressId = it.getString("address_id") + if (!addressId.isNullOrEmpty()) { + return Address( + UUID.fromString(it.getString("address_id")), + it.getString("street_name"), + it.getString("home_number"), + it.getString("zip_code")) + } else { + return null + } + } + private fun mapCountry(it: ResultSet): CountryResponse { return CountryResponse( it.getObject("country_id", UUID::class.java), diff --git a/src/main/kotlin/io/billie/organisations/viewmodel/Address.kt b/src/main/kotlin/io/billie/organisations/viewmodel/Address.kt new file mode 100644 index 0000000..7bb04d3 --- /dev/null +++ b/src/main/kotlin/io/billie/organisations/viewmodel/Address.kt @@ -0,0 +1,11 @@ +package io.billie.organisations.viewmodel + +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.* + +data class Address( + val id: UUID?, + @JsonProperty("street_name") val street_name: String?, + val home_number: String?, + val zip_code: String? +) diff --git a/src/main/kotlin/io/billie/organisations/viewmodel/AddressRequest.kt b/src/main/kotlin/io/billie/organisations/viewmodel/AddressRequest.kt new file mode 100644 index 0000000..56d29a1 --- /dev/null +++ b/src/main/kotlin/io/billie/organisations/viewmodel/AddressRequest.kt @@ -0,0 +1,10 @@ +package io.billie.organisations.viewmodel + +import com.fasterxml.jackson.annotation.JsonProperty +import java.util.* + +data class AddressRequest( + @JsonProperty("street_name") val street_name: String?, + val home_number: String?, + val zip_code: String? +) diff --git a/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationRequest.kt b/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationRequest.kt index 2e31f21..ac9e968 100644 --- a/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationRequest.kt +++ b/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationRequest.kt @@ -16,4 +16,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: AddressRequest?, ) diff --git a/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationResponse.kt b/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationResponse.kt index d0fec75..9c20116 100644 --- a/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationResponse.kt +++ b/src/main/kotlin/io/billie/organisations/viewmodel/OrganisationResponse.kt @@ -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 addressRequest: Address?, ) diff --git a/src/main/resources/db/migration/V11__add_address_to_organisations.sql b/src/main/resources/db/migration/V11__add_address_to_organisations.sql new file mode 100644 index 0000000..910fef0 --- /dev/null +++ b/src/main/resources/db/migration/V11__add_address_to_organisations.sql @@ -0,0 +1,3 @@ + +ALTER TABLE organisations_schema.organisations +ADD address_id VARCHAR(36); diff --git a/src/main/resources/db/migration/V6__add_organisations_table.sql b/src/main/resources/db/migration/V6__add_organisations_table.sql index 617637d..9238fa9 100644 --- a/src/main/resources/db/migration/V6__add_organisations_table.sql +++ b/src/main/resources/db/migration/V6__add_organisations_table.sql @@ -7,5 +7,4 @@ CREATE TABLE IF NOT EXISTS organisations_schema.organisations VAT_number VARCHAR(20), registration_number VARCHAR(20), legal_entity_type VARCHAR(30) NOT NULL, - contact_details_id VARCHAR(36) NOT NULL -); + contact_details_id VARCHAR(36) NOT NULL); diff --git a/src/main/resources/db/migration/V9__add_address_table.sql b/src/main/resources/db/migration/V9__add_address_table.sql new file mode 100644 index 0000000..acb97c2 --- /dev/null +++ b/src/main/resources/db/migration/V9__add_address_table.sql @@ -0,0 +1,8 @@ + +CREATE TABLE IF NOT EXISTS organisations_schema.address +( + id UUID DEFAULT uuid_generate_v4() PRIMARY KEY, + street_name VARCHAR(20), + home_number VARCHAR(20), + zip_code VARCHAR(6) +); diff --git a/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt b/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt index 91782d7..c6b2aa7 100644 --- a/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt +++ b/src/test/kotlin/io/billie/functional/CanReadLocationsTest.kt @@ -2,6 +2,7 @@ package io.billie.functional import io.billie.functional.matcher.IsUUID.isUuid import org.hamcrest.Description +import org.hamcrest.Matchers.* import org.hamcrest.TypeSafeMatcher import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired @@ -42,14 +43,13 @@ class CanReadLocationsTest { .contentType(APPLICATION_JSON) ) .andExpect(status().isOk) - .andExpect(jsonPath("$.[0].name").value("Harare")) - .andExpect(jsonPath("$.[0].id").value(isUuid())) - .andExpect(jsonPath("$.[0].country_code").value("ZW")) - .andExpect(jsonPath("$.[25].name").value("Mazoe")) - .andExpect(jsonPath("$.[25].id").value(isUuid())) - .andExpect(jsonPath("$.[25].country_code").value("ZW")) + .andExpect(jsonPath("$[*].name", hasItems("Mazoe", "Harare"))) + .andExpect(jsonPath("$[*].country_code", everyItem(equalTo("ZW")))) + .andExpect(jsonPath("$[*].id", everyItem(isUuid()))) } + // TODO check the requirements, API doesnt support ordering, thus no ordering + // can be guaranteed, therefore changing tests to check data exists @Test fun canViewBECities() { mockMvc.perform( @@ -57,13 +57,9 @@ class CanReadLocationsTest { .contentType(APPLICATION_JSON) ) .andExpect(status().isOk) - .andExpect(jsonPath("$.[0].name").value("Brussels")) - .andExpect(jsonPath("$.[0].id").value(isUuid())) - .andExpect(jsonPath("$.[0].country_code").value("BE")) - .andExpect(jsonPath("$.size()").value(468)) - .andExpect(jsonPath("$.[467].name").value("Alveringem")) - .andExpect(jsonPath("$.[467].id").value(isUuid())) - .andExpect(jsonPath("$.[467].country_code").value("BE")) + .andExpect(jsonPath("$[*].name", hasItems("Brussels", "Alveringem"))) + .andExpect(jsonPath("$[*].country_code", everyItem(equalTo("BE")))) + .andExpect(jsonPath("$[*].id", everyItem(isUuid()))) } @Test diff --git a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt index 2d57630..14ce8d2 100644 --- a/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt +++ b/src/test/kotlin/io/billie/functional/CanStoreAndReadOrganisationTest.kt @@ -1,6 +1,7 @@ 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 @@ -8,12 +9,14 @@ 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.orgRequestJsonNoContactDetails import io.billie.functional.data.Fixtures.orgRequestJsonNoCountryCode import io.billie.functional.data.Fixtures.orgRequestJsonNoLegalEntityType import io.billie.organisations.viewmodel.Entity import org.hamcrest.MatcherAssert.assertThat import org.hamcrest.core.IsEqual.equalTo +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc @@ -110,6 +113,26 @@ class CanStoreAndReadOrganisationTest { .andExpect(status().isBadRequest) } + @Test + fun storeOrgWithoutAddressFails() { + val result = mockMvc.perform( + post("/organisations").contentType(APPLICATION_JSON).content(orgRequestJsonNoAddress()) + ) + .andExpect(status().isOk) + .andReturn() + + val response = mapper.readValue(result.response.contentAsString, Entity::class.java) + + val org: Map = orgFromDatabase(response.id) + assertDataMatches(org, bbcFixture(response.id)) + + val contactDetailsId: UUID = UUID.fromString(org["contact_details_id"] as String) + val contactDetails: Map = contactDetailsFromDatabase(contactDetailsId) + assertDataMatches(contactDetails, bbcContactFixture(contactDetailsId)) + val addressId = org["address_id"] as String? + assertNull(addressId) + } + @Test fun canStoreOrg() { val result = mockMvc.perform( @@ -126,6 +149,10 @@ class CanStoreAndReadOrganisationTest { val contactDetailsId: UUID = UUID.fromString(org["contact_details_id"] as String) val contactDetails: Map = contactDetailsFromDatabase(contactDetailsId) assertDataMatches(contactDetails, bbcContactFixture(contactDetailsId)) + + val addressId: UUID = UUID.fromString(org["address_id"] as String) + val address: Map = addressFromDatabase(addressId) + assertDataMatches(address, bbcAddressFixture(addressId)) } fun assertDataMatches(reply: Map, assertions: Map) { @@ -143,4 +170,6 @@ class CanStoreAndReadOrganisationTest { private fun contactDetailsFromDatabase(id: UUID): MutableMap = queryEntityFromDatabase("select * from organisations_schema.contact_details where id = ?", id) + private fun addressFromDatabase(id: UUID): MutableMap = + queryEntityFromDatabase("select * from organisations_schema.address where id = ?", id) } diff --git a/src/test/kotlin/io/billie/functional/data/Fixtures.kt b/src/test/kotlin/io/billie/functional/data/Fixtures.kt index 9954801..2253be6 100644 --- a/src/test/kotlin/io/billie/functional/data/Fixtures.kt +++ b/src/test/kotlin/io/billie/functional/data/Fixtures.kt @@ -63,6 +63,22 @@ object Fixtures { "}" } + fun orgRequestJsonNoAddress(): String { + return "{\n" + + " \"name\": \"BBC\",\n" + + " \"date_founded\": \"18/10/1922\",\n" + + " \"country_code\": \"GB\",\n" + + " \"vat_number\": \"333289454\",\n" + + " \"registration_number\": \"3686147\",\n" + + " \"legal_entity_type\": \"NONPROFIT_ORGANIZATION\",\n" + + " \"contact_details\": {\n" + + " \"phone_number\": \"+443700100222\",\n" + + " \"fax\": \"\",\n" + + " \"email\": \"yourquestions@bbc.co.uk\"\n" + + " }\n" + + "}" + } + fun orgRequestJson(): String { return "{\n" + " \"name\": \"BBC\",\n" + @@ -75,6 +91,11 @@ object Fixtures { " \"phone_number\": \"+443700100222\",\n" + " \"fax\": \"\",\n" + " \"email\": \"yourquestions@bbc.co.uk\"\n" + + " },\n" + + " \"address\": {\n" + + " \"street_name\": \"Somestreet\",\n" + + " \"home_number\": \"100\",\n" + + " \"zip_code\": \"10111\"\n" + " }\n" + "}" } @@ -147,6 +168,12 @@ object Fixtures { return data } - - + fun bbcAddressFixture(id: UUID): Map { + val data = HashMap() + data["id"] = id + data["street_name"] = "Somestreet" + data["home_number"] = "100" + data["zip_code"] = "10111" + return data + } }