Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PRSD-353: Registration Number Service #10

Merged
merged 27 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2f01778
PRSD-353: Differentiates between AuditableEntity and ModifiableAudita…
isobel-softwire Oct 11, 2024
46b7719
PRSD-353: Creates RegistrationNumber and (skeleton) Landlord entities
isobel-softwire Oct 11, 2024
8eb71f0
PRSD-353: Adds RegistrationNumberService.createRegistrationNumber
isobel-softwire Oct 11, 2024
c538baa
PRSD-353: Creates RegistrationNumberService.retrieveEntity
isobel-softwire Oct 14, 2024
c9211fc
PRSD-353: Creates ServiceTest base class
isobel-softwire Oct 14, 2024
2eac231
fixup! PRSD-353: Adds RegistrationNumberService.createRegistrationNumber
isobel-softwire Oct 15, 2024
ee25138
fixup! PRSD-353: Adds RegistrationNumberService.createRegistrationNumber
isobel-softwire Oct 15, 2024
472c58c
fixup! PRSD-353: Adds RegistrationNumberService.createRegistrationNumber
isobel-softwire Oct 15, 2024
a5cf137
PRSD-353: Creates RegistrationNumberService.formatRegNum
isobel-softwire Oct 15, 2024
749b610
PRSD-353: Displays an example formatted landlord registration number
isobel-softwire Oct 15, 2024
f53ce37
fixup! PRSD-353: Creates RegistrationNumberService.retrieveEntity
isobel-softwire Oct 15, 2024
36b76f5
PRSD-353: Moves enums package into constants
isobel-softwire Oct 15, 2024
86c2034
PRSD-353: Uses TIMESTAMPTZ instead of TIMESTAMP WITHOUT TIMEZONE
isobel-softwire Oct 15, 2024
ecbd3b9
PRSD-353: Puts RegistrationNumberService.createRegistrationNumber in …
isobel-softwire Oct 16, 2024
b52762b
PRSD-353: Corrects registration number charset
isobel-softwire Oct 16, 2024
2cb5629
PRSD-353: Makes RegistrationNumberService rely more on constants
isobel-softwire Oct 16, 2024
09da348
PRSD-353: Removes ServiceTest base class
isobel-softwire Oct 16, 2024
8d02be8
PRSD-353: Tests that RegistrationNumberType initials are unique
isobel-softwire Oct 16, 2024
af36ed1
PRSD-353: Corrects reg_num_and_landlord_skeleton to be a minor migration
isobel-softwire Oct 16, 2024
c676902
PRSD-353: Removes RegisteredEntity interface
isobel-softwire Oct 16, 2024
8931a08
PRSD-353: Renames RegistrationNumberService.formatRegNum
isobel-softwire Oct 16, 2024
ed06dde
PRSD-353: Refactors RegistrationNumberService to parse a string into …
isobel-softwire Oct 16, 2024
82e46f0
PRSD-353: Throws an error on attempt to parse an invalid registration…
isobel-softwire Oct 16, 2024
14915d6
PRSD-353: Refactors and updates RegistrationNumberServiceTests
isobel-softwire Oct 16, 2024
248a3ca
PRSD-353: Creates LandlordService.retrieveLandlordByRegNum
isobel-softwire Oct 16, 2024
7d50980
fixup! PRSD-353: Renames RegistrationNumberService.formatRegNum
isobel-softwire Oct 16, 2024
20df151
PRSD-353: Moves data presentation functions from RegistrationNumberSe…
isobel-softwire Oct 17, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package uk.gov.communities.prsdb.webapp.constants

import kotlin.math.pow

val REG_NUM_CHARSET =
listOf(
'C',
'D',
'F',
'G',
'H',
'J',
'K',
'M',
'N',
'P',
'Q',
'R',
'S',
'T',
'V',
'W',
'X',
'Y',
'Z',
'2',
'3',
'4',
'5',
'6',
'7',
'9',
)

val REG_NUM_BASE = REG_NUM_CHARSET.size

const val REG_NUM_LENGTH = 8

const val REG_NUM_SEG_LENGTH = REG_NUM_LENGTH / 2

const val MIN_REG_NUM = 0L

val MAX_REG_NUM = REG_NUM_BASE.toDouble().pow(REG_NUM_LENGTH).toLong() - 1
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package uk.gov.communities.prsdb.webapp.constants.enums

enum class RegistrationNumberType {
PROPERTY,
LANDLORD,
AGENT,
;

fun toInitial(): Char = this.toString()[0]

companion object {
fun initialToType(initial: Char): RegistrationNumberType =
when (initial) {
'P' -> PROPERTY
'L' -> LANDLORD
'A' -> AGENT
else -> throw IllegalArgumentException("Invalid Initial")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import uk.gov.communities.prsdb.webapp.constants.SERVICE_NAME
import uk.gov.communities.prsdb.webapp.constants.enums.RegistrationNumberType
import uk.gov.communities.prsdb.webapp.models.dataModels.RegistrationNumberDataModel

@Controller
@RequestMapping("/")
Expand All @@ -14,6 +16,10 @@ class ExampleStartPageController {
model.addAttribute("contentHeader", "Welcome to the Private Rental Sector Database")
model.addAttribute("title", "Private Rental Sector Database")
model.addAttribute("serviceName", SERVICE_NAME)
model.addAttribute(
"landlordRegNum",
RegistrationNumberDataModel(RegistrationNumberType.LANDLORD, 205498766).toString(),
)
model.addAttribute("startButtonHref", "/registration")
model.addAttribute("startButtonText", "Start now")
return "demoStart"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import jakarta.persistence.MappedSuperclass
import jakarta.persistence.Temporal
import jakarta.persistence.TemporalType
import org.springframework.data.annotation.CreatedDate
import org.springframework.data.annotation.LastModifiedDate
import java.io.Serializable
import java.time.OffsetDateTime

Expand All @@ -16,10 +15,4 @@ abstract class AuditableEntity : Serializable {
@Column(updatable = false)
lateinit var createdDate: OffsetDateTime
private set

@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
@Column(insertable = false)
lateinit var lastModifiedDate: OffsetDateTime
private set
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package uk.gov.communities.prsdb.webapp.database.entity

import jakarta.persistence.Entity
import jakarta.persistence.ForeignKey
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.OneToOne

@Entity
class Landlord {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private val id: Long? = null

@OneToOne(optional = false)
@JoinColumn(
name = "registration_number_id",
nullable = false,
foreignKey = ForeignKey(name = "FK_LANDLORD_REG_NUM"),
)
lateinit var registrationNumber: RegistrationNumber
private set
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import jakarta.persistence.OneToOne
import java.util.Date

@Entity
class LandlordUser : AuditableEntity() {
class LandlordUser : ModifiableAuditableEntity() {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private val id: Long? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import jakarta.persistence.GenerationType
import jakarta.persistence.Id

@Entity
class LocalAuthority : AuditableEntity() {
class LocalAuthority : ModifiableAuditableEntity() {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private val id: Int? = null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import jakarta.persistence.JoinColumn
import jakarta.persistence.OneToOne

@Entity
class LocalAuthorityUser : AuditableEntity() {
class LocalAuthorityUser : ModifiableAuditableEntity() {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private val id: Long? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package uk.gov.communities.prsdb.webapp.database.entity

import jakarta.persistence.Column
import jakarta.persistence.MappedSuperclass
import jakarta.persistence.Temporal
import jakarta.persistence.TemporalType
import org.springframework.data.annotation.LastModifiedDate
import java.io.Serializable
import java.time.OffsetDateTime

@MappedSuperclass
abstract class ModifiableAuditableEntity :
AuditableEntity(),
Serializable {
@LastModifiedDate
@Temporal(TemporalType.TIMESTAMP)
@Column(insertable = false)
lateinit var lastModifiedDate: OffsetDateTime
private set
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import jakarta.persistence.Entity
import jakarta.persistence.Id

@Entity
class OneLoginUser : AuditableEntity() {
class OneLoginUser : ModifiableAuditableEntity() {
@Id
private val id: String? = null

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package uk.gov.communities.prsdb.webapp.database.entity

import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import uk.gov.communities.prsdb.webapp.constants.enums.RegistrationNumberType

@Entity
class RegistrationNumber() : AuditableEntity() {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private val id: Long? = null

@Column(nullable = false, unique = true)
var number: Long? = null
private set

@Column(nullable = false)
lateinit var type: RegistrationNumberType
private set

constructor(type: RegistrationNumberType, number: Long) : this() {
this.number = number
this.type = type
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package uk.gov.communities.prsdb.webapp.database.repository

import org.springframework.data.jpa.repository.JpaRepository
import uk.gov.communities.prsdb.webapp.database.entity.Landlord

interface LandlordRepository : JpaRepository<Landlord?, Long?> {
// The underscore tells JPA to access fields relating to the referenced table
@Suppress("ktlint:standard:function-naming")
fun findByRegistrationNumber_Number(registrationNumber: Long): Landlord?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package uk.gov.communities.prsdb.webapp.database.repository

import org.springframework.data.jpa.repository.JpaRepository
import uk.gov.communities.prsdb.webapp.database.entity.RegistrationNumber

interface RegistrationNumberRepository : JpaRepository<RegistrationNumber?, Long?> {
fun existsByNumber(number: Long): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package uk.gov.communities.prsdb.webapp.models.dataModels

import uk.gov.communities.prsdb.webapp.constants.REG_NUM_BASE
import uk.gov.communities.prsdb.webapp.constants.REG_NUM_CHARSET
import uk.gov.communities.prsdb.webapp.constants.REG_NUM_LENGTH
import uk.gov.communities.prsdb.webapp.constants.REG_NUM_SEG_LENGTH
import uk.gov.communities.prsdb.webapp.constants.enums.RegistrationNumberType

data class RegistrationNumberDataModel(
val type: RegistrationNumberType,
val number: Long,
) {
companion object {
fun parseRegNum(regNumString: String): RegistrationNumberDataModel {
val baseRegNumString = getBaseRegNumString(regNumString)

validateBaseRegNumString(baseRegNumString)

val regNumType = RegistrationNumberType.initialToType(baseRegNumString[0])

var regNumNumber = 0L
for (char in baseRegNumString.substring(1)) {
regNumNumber = REG_NUM_BASE * regNumNumber + REG_NUM_CHARSET.indexOf(char)
}

return RegistrationNumberDataModel(regNumType, regNumNumber)
}

private fun getBaseRegNumString(regNumString: String): String = regNumString.filter { it.isLetterOrDigit() }.uppercase()

private fun validateBaseRegNumString(baseRegNumString: String) {
if (baseRegNumString.length != REG_NUM_LENGTH + 1) {
throw IllegalArgumentException("Invalid registration number string length")
}
if (baseRegNumString.substring(1).any { !REG_NUM_CHARSET.contains(it) }) {
throw IllegalArgumentException("Invalid registration number string characters")
}
}
}

override fun toString(): String {
var regNumString = ""
var quotient = this.number
while (quotient > 0) {
regNumString = REG_NUM_CHARSET[(quotient % REG_NUM_BASE).toInt()] + regNumString
quotient /= REG_NUM_BASE
}
regNumString = regNumString.padStart(REG_NUM_LENGTH, REG_NUM_CHARSET[0])

return this.type.toInitial() +
"-" +
regNumString.substring(0, REG_NUM_SEG_LENGTH) +
"-" +
regNumString.substring(REG_NUM_SEG_LENGTH)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package uk.gov.communities.prsdb.webapp.services

import org.springframework.stereotype.Service
import uk.gov.communities.prsdb.webapp.constants.enums.RegistrationNumberType
import uk.gov.communities.prsdb.webapp.database.entity.Landlord
import uk.gov.communities.prsdb.webapp.database.repository.LandlordRepository
import uk.gov.communities.prsdb.webapp.models.dataModels.RegistrationNumberDataModel

@Service
class LandlordService(
val landlordRepository: LandlordRepository,
) {
fun retrieveLandlordByRegNum(regNum: RegistrationNumberDataModel): Landlord? {
if (regNum.type != RegistrationNumberType.LANDLORD) {
throw IllegalArgumentException("Invalid registration number type")
}
return landlordRepository.findByRegistrationNumber_Number(regNum.number)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package uk.gov.communities.prsdb.webapp.services

import jakarta.transaction.Transactional
import org.springframework.stereotype.Service
import uk.gov.communities.prsdb.webapp.constants.MAX_REG_NUM
import uk.gov.communities.prsdb.webapp.constants.MIN_REG_NUM
import uk.gov.communities.prsdb.webapp.constants.enums.RegistrationNumberType
import uk.gov.communities.prsdb.webapp.database.entity.RegistrationNumber
import uk.gov.communities.prsdb.webapp.database.repository.RegistrationNumberRepository

@Service
class RegistrationNumberService(
val regNumRepository: RegistrationNumberRepository,
) {
@Transactional
fun createRegistrationNumber(type: RegistrationNumberType) {
regNumRepository.save(RegistrationNumber(type, generateUniqueRegNum()))
}

private fun generateUniqueRegNum(): Long {
var registrationNumber: Long
do {
registrationNumber = (MIN_REG_NUM..MAX_REG_NUM).random()
} while (regNumRepository.existsByNumber(registrationNumber))
return registrationNumber
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
CREATE TABLE landlord
(
id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
registration_number_id BIGINT NOT NULL,
CONSTRAINT pk_landlord PRIMARY KEY (id)
);

CREATE TABLE registration_number
(
id BIGINT GENERATED BY DEFAULT AS IDENTITY NOT NULL,
created_date TIMESTAMPTZ(6),
number BIGINT NOT NULL,
type SMALLINT NOT NULL,
CONSTRAINT pk_registrationnumber PRIMARY KEY (id)
);

ALTER TABLE landlord
ADD CONSTRAINT uc_landlord_registration_number UNIQUE (registration_number_id);

ALTER TABLE registration_number
ADD CONSTRAINT uc_registrationnumber_number UNIQUE (number);

ALTER TABLE landlord
ADD CONSTRAINT FK_LANDLORD_REG_NUM FOREIGN KEY (registration_number_id) REFERENCES registration_number (id);
4 changes: 4 additions & 0 deletions src/main/resources/templates/demoStart.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@
<html th:replace="~{fragments/layout :: layout(${title}, ${serviceName}, ~{::main})}">
<main class="govuk-main-wrapper" id="main-content">
<h1 class="govuk-heading-xl" th:text="${contentHeader}">Default page template</h1>
<p class="govuk-body">
<span>Example Landlord Registration Number: </span>
<span th:text="${landlordRegNum}">Example Landlord Registration Number</span>
</p>
<div th:replace="~{fragments/startButton :: button( ${startButtonHref}, ${startButtonText} )}"></div>
</main>
</html>
Loading