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
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package uk.gov.communities.prsdb.webapp.constants

const val LOCAL_COUNCIL_INVITATION_LIFETIME_IN_HOURS: Int = 48
const val JOINT_LANDLORD_INVITATION_LIFETIME_IN_HOURS: Int = 672 // 28 days
const val JOINT_LANDLORD_INVITATION_LIFETIME_IN_DAYS: Int = 28
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,22 @@ import org.springframework.web.servlet.ModelAndView
import org.springframework.web.util.UriTemplate
import uk.gov.communities.prsdb.webapp.annotations.webAnnotations.PrsdbController
import uk.gov.communities.prsdb.webapp.config.interceptors.BackLinkInterceptor.Companion.overrideBackLinkForUrl
import uk.gov.communities.prsdb.webapp.config.managers.FeatureFlagManager
import uk.gov.communities.prsdb.webapp.constants.COMPLIANCE_INFO_FRAGMENT
import uk.gov.communities.prsdb.webapp.constants.JOINT_LANDLORDS
import uk.gov.communities.prsdb.webapp.constants.LANDLORD_DETAILS_FRAGMENT
import uk.gov.communities.prsdb.webapp.constants.LANDLORD_PATH_SEGMENT
import uk.gov.communities.prsdb.webapp.constants.LOCAL_COUNCIL_PATH_SEGMENT
import uk.gov.communities.prsdb.webapp.constants.PROPERTY_DETAILS_SEGMENT
import uk.gov.communities.prsdb.webapp.controllers.LandlordController.Companion.LANDLORD_DASHBOARD_URL
import uk.gov.communities.prsdb.webapp.controllers.LocalCouncilDashboardController.Companion.LOCAL_COUNCIL_DASHBOARD_URL
import uk.gov.communities.prsdb.webapp.helpers.DateTimeHelper
import uk.gov.communities.prsdb.webapp.models.viewModels.InvitationViewModelBuilder
import uk.gov.communities.prsdb.webapp.models.viewModels.summaryModels.PropertyDetailsLandlordViewModelBuilder
import uk.gov.communities.prsdb.webapp.models.viewModels.summaryModels.PropertyDetailsViewModel
import uk.gov.communities.prsdb.webapp.models.viewModels.summaryModels.propertyComplianceViewModels.PropertyComplianceViewModelFactory
import uk.gov.communities.prsdb.webapp.services.BackUrlStorageService
import uk.gov.communities.prsdb.webapp.services.JointLandlordInvitationService
import uk.gov.communities.prsdb.webapp.services.PropertyComplianceService
import uk.gov.communities.prsdb.webapp.services.PropertyOwnershipService
import java.security.Principal
Expand All @@ -36,6 +40,8 @@ class PropertyDetailsController(
private val propertyComplianceService: PropertyComplianceService,
private val propertyComplianceViewModelFactory: PropertyComplianceViewModelFactory,
private val messageSource: MessageSource,
private val jointLandlordInvitationService: JointLandlordInvitationService,
private val featureFlagManager: FeatureFlagManager,
) {
@PreAuthorize("hasRole('LANDLORD')")
@GetMapping(LANDLORD_PROPERTY_DETAILS_ROUTE)
Expand Down Expand Up @@ -82,6 +88,23 @@ class PropertyDetailsController(
modelAndView.addObject("deregisterPropertyLink", DeregisterPropertyController.getPropertyDeregistrationPath(propertyOwnershipId))
modelAndView.addObject("isLandlordView", true)
modelAndView.addObject("backUrl", LANDLORD_DASHBOARD_URL)

val isJointLandlordsEnabled = featureFlagManager.checkFeature(JOINT_LANDLORDS)
modelAndView.addObject("isJointLandlordsEnabled", isJointLandlordsEnabled)
if (isJointLandlordsEnabled) {
val (pendingInvitations, expiredInvitations) =
jointLandlordInvitationService
.getPendingAndExpiredInvitations(propertyOwnership)
.let { (pending, expired) ->
Pair(
pending.map { InvitationViewModelBuilder.buildPendingViewModel(it) },
expired.map { InvitationViewModelBuilder.buildExpiredViewModel(it) },
)
}
modelAndView.addObject("pendingInvitations", pendingInvitations)
modelAndView.addObject("expiredInvitations", expiredInvitations)
}

return modelAndView
}

Expand Down Expand Up @@ -134,6 +157,7 @@ class PropertyDetailsController(
model.addAttribute("complianceDetails", propertyComplianceDetails)
model.addAttribute("complianceInfoTabId", COMPLIANCE_INFO_FRAGMENT)
model.addAttribute("isLandlordView", false)
model.addAttribute("isJointLandlordsEnabled", false)
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this may change - right now assuming that LCs won't be able to view pending/expired invites though design to input on what should happen here

model.addAttribute("backUrl", LOCAL_COUNCIL_DASHBOARD_URL)

return "propertyDetailsView"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.JoinColumn
import jakarta.persistence.ManyToOne
import kotlinx.datetime.DatePeriod
import kotlinx.datetime.plus
import kotlinx.datetime.toKotlinInstant
import uk.gov.communities.prsdb.webapp.constants.JOINT_LANDLORD_INVITATION_LIFETIME_IN_DAYS
import uk.gov.communities.prsdb.webapp.helpers.DateTimeHelper
import java.time.Instant
import java.time.temporal.ChronoUnit
import java.util.UUID

@Entity
Expand All @@ -33,6 +40,24 @@ class JointLandlordInvitation(
lateinit var invitingLandlord: Landlord
private set

val expiryDate: Instant
get() = createdDate.plus(JOINT_LANDLORD_INVITATION_LIFETIME_IN_DAYS.toLong(), ChronoUnit.DAYS)

val daysUntilExpiry: Long
get() = ChronoUnit.DAYS.between(Instant.now(), expiryDate).coerceAtLeast(0)

val isExpired: Boolean
get() {
val dateTimeHelper = DateTimeHelper()

val expiresOnDate =
DateTimeHelper
.getDateInUK(createdDate.toKotlinInstant())
.plus(DatePeriod(days = JOINT_LANDLORD_INVITATION_LIFETIME_IN_DAYS))

return dateTimeHelper.getCurrentDateInUK() > expiresOnDate
}

Comment on lines +43 to +60
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm just slightly nervous that we have some non-trivial date arithmetic here that all ought to agree. Might be worth some tests for this to check that they all behave as expected, and consistently with each other? Especially at common problem times, like near midnight or near a DST change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

see #1401 which I'll rebase in when it's merged, there will be some stringent tests for this

constructor(
token: UUID,
email: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@ package uk.gov.communities.prsdb.webapp.database.repository

import org.springframework.data.jpa.repository.JpaRepository
import uk.gov.communities.prsdb.webapp.database.entity.JointLandlordInvitation
import uk.gov.communities.prsdb.webapp.database.entity.PropertyOwnership
import java.util.UUID

interface JointLandlordInvitationRepository : JpaRepository<JointLandlordInvitation, Long> {
fun findByToken(token: UUID): JointLandlordInvitation?

fun findByRegisteredOwnership(propertyOwnership: PropertyOwnership): List<JointLandlordInvitation>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package uk.gov.communities.prsdb.webapp.models.viewModels

import kotlinx.datetime.toJavaLocalDate
import kotlinx.datetime.toKotlinInstant
import uk.gov.communities.prsdb.webapp.database.entity.JointLandlordInvitation
import uk.gov.communities.prsdb.webapp.helpers.DateTimeHelper
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util.Locale

data class PendingInvitationViewModel(
val email: String,
val expiresInDays: Long,
val expiryDate: String,
val sentDate: String,
)

data class ExpiredInvitationViewModel(
val email: String,
val expiredDate: String,
)

class InvitationViewModelBuilder {
companion object {
private val DATE_FORMATTER = DateTimeFormatter.ofPattern("d MMMM yyyy", Locale.UK)

fun buildPendingViewModel(invitation: JointLandlordInvitation): PendingInvitationViewModel =
PendingInvitationViewModel(
email = invitation.invitedEmail,
expiresInDays = invitation.daysUntilExpiry,
expiryDate = formatInstant(invitation.expiryDate),
sentDate = formatInstant(invitation.createdDate),
)

fun buildExpiredViewModel(invitation: JointLandlordInvitation): ExpiredInvitationViewModel =
ExpiredInvitationViewModel(
email = invitation.invitedEmail,
expiredDate = formatInstant(invitation.expiryDate),
)

private fun formatInstant(instant: Instant): String =
DateTimeHelper.getDateInUK(instant.toKotlinInstant()).toJavaLocalDate().format(DATE_FORMATTER)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@ class JointLandlordInvitationService(
private val absoluteUrlProvider: AbsoluteUrlProvider,
private val session: HttpSession,
) {
fun getPendingAndExpiredInvitations(
propertyOwnership: PropertyOwnership,
): Pair<List<JointLandlordInvitation>, List<JointLandlordInvitation>> {
val (expired, pending) =
invitationRepository
.findByRegisteredOwnership(propertyOwnership)
.sortedByDescending { it.createdDate }
.partition { it.isExpired }
return Pair(pending, expired) // flips the above pair from expired, pending to pending, expired
}

fun sendInvitationEmails(
jointLandlordEmails: List<String>,
propertyOwnership: PropertyOwnership,
Expand Down
19 changes: 19 additions & 0 deletions src/main/resources/css/_linkGroupList.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
@import "govuk/base";

.prsdb-link-group-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;

.prsdb-link-group-item:first-child {
padding: 0;
margin: 0;
}

.prsdb-link-group-item:not(:first-child) {
margin-left: govuk-spacing(2);
padding-left: govuk-spacing(2);
border-left: 1px solid govuk-colour("mid-grey");
}
}
1 change: 1 addition & 0 deletions src/main/resources/css/custom.scss
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
@use "dashboard";
@use "deregisterLinks";
@use "linkGroupList";
13 changes: 13 additions & 0 deletions src/main/resources/messages/propertyDetails.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,19 @@ landlordDetails:
contactNumber: Contact number
addressNonEnglandOrWales: Address (outside England or Wales)
contactAddressInEnglandOrWales: Contact address in England or Wales
invitations:
pendingInvitations:
heading: 'Pending invitations ({0})'
expiresIn: 'Expires in {0} days ({1})'
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to worry about "1 days" and "0 days"?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suspect we do, have added alternate copy for this

I don't think any of these other strings need a singular equivalent

expiresInSingular: 'Expires in 1 day ({0})'
sentOn: 'Sent on {0}'
sendNewInvitationEmail: Send a new email invitation
cancelInvitation: Cancel invitation
expiredInvitations:
heading: 'Expired invitations ({0})'
expiredOn: 'Expired on {0}'
sendNewInvitationEmail: Send a new invitation email
removeFromList: Remove from list
complianceInformation:
heading: Compliance information
certificateStatus: Certificate status
Expand Down
80 changes: 80 additions & 0 deletions src/main/resources/templates/propertyDetailsView.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,86 @@ <h3 class="govuk-heading-s" th:text="#{propertyDetails.propertyRecord.tenancyAnd
<h2 class="govuk-heading-m" th:text="#{propertyDetails.landlordDetails.heading}">propertyDetails.landlordDetails.heading</h2>
<h3 class="govuk-heading-s" th:text="#{propertyDetails.landlordDetails.registeredLandlord.heading}">propertyDetails.landlordDetails.registeredLandlord.heading</h3>
<dl th:replace="~{fragments/summaryList :: summaryList(${landlordDetails})}"></dl>

<th:block th:if="${isJointLandlordsEnabled}">
<details th:if="${pendingInvitations != null && !pendingInvitations.isEmpty()}" class="govuk-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text"
th:text="#{propertyDetails.landlordDetails.invitations.pendingInvitations.heading(${#lists.size(pendingInvitations)})}">
propertyDetails.landlordDetails.invitations.pendingInvitations.heading
</span>
</summary>
<div class="govuk-details__text">
<th:block th:each="invitation, iterStat : ${pendingInvitations}">
<hr th:unless="${iterStat.first}" class="govuk-section-break govuk-section-break--m govuk-section-break--visible">
<div class="govuk-!-margin-bottom-4">
<p class="govuk-body govuk-!-font-weight-bold govuk-!-margin-bottom-1" th:text="${invitation.email}">invitation.email</p>
<p class="govuk-body govuk-!-margin-bottom-1"
th:if="${invitation.expiresInDays != 1}"
th:text="#{propertyDetails.landlordDetails.invitations.pendingInvitations.expiresIn(${invitation.expiresInDays}, ${invitation.expiryDate})}">
propertyDetails.landlordDetails.invitations.pendingInvitations.expiresIn
</p>
<p class="govuk-body govuk-!-margin-bottom-1"
th:if="${invitation.expiresInDays == 1}"
th:text="#{propertyDetails.landlordDetails.invitations.pendingInvitations.expiresInSingular(${invitation.expiryDate})}">
propertyDetails.landlordDetails.invitations.pendingInvitations.expiresInSingular
</p>
<p class="govuk-body govuk-!-margin-bottom-1"
th:text="#{propertyDetails.landlordDetails.invitations.pendingInvitations.sentOn(${invitation.sentDate})}">
propertyDetails.landlordDetails.invitations.pendingInvitations.sentOn
</p>
<ul class="prsdb-link-group-list">
<li class="prsdb-link-group-item">
<!--/* TODO: PDJB-301 - Implement send new invitation action */-->
<a class="govuk-link" href="#" th:text="#{propertyDetails.landlordDetails.invitations.pendingInvitations.sendNewInvitationEmail}">
propertyDetails.landlordDetails.invitations.pendingInvitations.sendNewInvitationEmail
</a>
</li>
<li class="prsdb-link-group-item">
<!--/* TODO: PDJB-303 - Implement invite cancellation action */-->
<a class="govuk-link" href="#" th:text="#{propertyDetails.landlordDetails.invitations.pendingInvitations.cancelInvitation}">
propertyDetails.landlordDetails.invitations.pendingInvitations.cancelInvitation
</a>
</li>
</ul>
</div>
</th:block>
</div>
</details>

<details th:if="${expiredInvitations != null && !expiredInvitations.isEmpty()}" class="govuk-details">
<summary class="govuk-details__summary">
<span class="govuk-details__summary-text"
th:text="#{propertyDetails.landlordDetails.invitations.expiredInvitations.heading(${#lists.size(expiredInvitations)})}">
propertyDetails.landlordDetails.invitations.expiredInvitations.heading
</span>
</summary>
<div class="govuk-details__text">
<th:block th:each="invitation, iterStat : ${expiredInvitations}">
<hr th:unless="${iterStat.first}" class="govuk-section-break govuk-section-break--m govuk-section-break--visible">
<div class="govuk-!-margin-bottom-4">
<p class="govuk-body govuk-!-font-weight-bold govuk-!-margin-bottom-1" th:text="${invitation.email}">invitation.email</p>
<p class="govuk-body govuk-!-margin-bottom-1"
th:text="#{propertyDetails.landlordDetails.invitations.expiredInvitations.expiredOn(${invitation.expiredDate})}">
propertyDetails.landlordDetails.invitations.expiredInvitations.expiredOn
</p>
<ul class="prsdb-link-group-list">
<li class="prsdb-link-group-item">
<!--/* TODO: PDJB-301 - Implement send new invitation action */-->
<a class="govuk-link" href="#"
th:text="#{propertyDetails.landlordDetails.invitations.expiredInvitations.sendNewInvitationEmail}">propertyDetails.landlordDetails.invitations.expiredInvitations.sendNewInvitationEmail</a>
</li>
<li class="prsdb-link-group-item">
<!--/* TODO: PDJB-302 - Implement remove from list action */-->
<a class="govuk-link" href="#"
th:text="#{propertyDetails.landlordDetails.invitations.expiredInvitations.removeFromList}">propertyDetails.landlordDetails.invitations.expiredInvitations.removeFromList</a>
</li>
</ul>
</div>
</th:block>
</div>
</details>
</th:block>
</div>

<div id="compliance-information-panel" th:replace="~{fragments/tabs/tabsPanel :: tabsPanel(${complianceInfoTabId}, ~{::#compliance-information-panel/content()})}">
Expand Down
Loading
Loading