Skip to content
Merged
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
Expand Up @@ -298,6 +298,8 @@ void rollbackRent(@NotNull String worldGuardRegionId,
sealed interface UnrentResult {
record Success(double refund, @NotNull UUID tenantId, @NotNull UUID landlordId) implements UnrentResult {}
record NoLeaseholdContract() implements UnrentResult {}
/** The lease is scheduled for termination; it can only end via the sweep on the effective date. */
record Terminating() implements UnrentResult {}
record UpdateFailed() implements UnrentResult {}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,12 @@ public void rollbackRent(@NotNull String worldGuardRegionId,
if (lease == null) {
return new UnrentResult.NoLeaseholdContract();
}
// A scheduled termination — an eviction in particular — must run its notice period out.
// Without this guard the tenant could unrent (which clears terminationEffectiveDate along
// with the tenant) and immediately re-rent, unilaterally cancelling their own eviction.
if (lease.terminationEffectiveDate() != null) {
return new UnrentResult.Terminating();
}
long totalSeconds = lease.durationSeconds();
// Pro-rata refund of the UNUSED portion of the current period. Renewals push
// endDate out by another durationSeconds each (see renewLeasehold) but are not
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ int scheduleTermination(@NotNull String worldGuardRegionId,
/** Leaseholds whose scheduled termination date has elapsed, due to be ended by the sweep. */
@NotNull List<TerminatedLeaseholdView> selectTerminatedLeaseholds();

/** Clears the tenant, guarded on the caller still being the tenant and no termination pending. */
int unrentRegion(@NotNull String worldGuardRegionId,
@NotNull UUID worldId,
@NotNull UUID tenantId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ int clearTermination(@Param("worldGuardRegionId") @NotNull String worldGuardRegi
WHERE rr.worldGuardRegionId = #{worldGuardRegionId}
AND rr.worldId = #{worldId}
AND lc.tenantId = #{tenantId}
AND lc.terminationEffectiveDate IS NULL
""")
int unrentRegion(@Param("worldGuardRegionId") @NotNull String worldGuardRegionId,
@Param("worldId") @NotNull UUID worldId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import io.github.md5sha256.realty.api.RealtyBackend.PayOfferResult;
import io.github.md5sha256.realty.api.RealtyBackend.RegionInfo;
import io.github.md5sha256.realty.database.entity.FreeholdContractBidPaymentEntity;
import io.github.md5sha256.realty.database.entity.LeaseholdContractEntity;
import io.github.md5sha256.realty.database.entity.FreeholdContractOfferPaymentEntity;
import org.apache.ibatis.session.SqlSession;
import org.junit.jupiter.api.Assertions;
Expand Down Expand Up @@ -320,6 +321,30 @@ void scheduleBlocksExtensionAndSweepEndsLease() {
Assertions.assertNull(logic.getLeaseholdContract(regionId, WORLD_ID).tenantId());
}

@Test
@DisplayName("a tenant under eviction cannot unrent their way out and re-rent")
void unrentBlockedWhileTerminating() {
String regionId = uniqueRegionId();
logic.createLeasehold(regionId, WORLD_ID, 200.0, 3600, 5, PLAYER_A);
logic.rentRegion(regionId, WORLD_ID, PLAYER_B);

LocalDateTime effective = LocalDateTime.now().plusDays(7);
Assertions.assertInstanceOf(RealtyBackend.TerminateLeaseholdResult.Success.class,
logic.terminateLease(regionId, WORLD_ID, effective, effective, "landlord"));

// Pre-fix this succeeded, clearing the termination along with the tenant.
Assertions.assertInstanceOf(RealtyBackend.UnrentResult.Terminating.class,
logic.unrentRegion(regionId, WORLD_ID, PLAYER_B));

LeaseholdContractEntity lease = logic.getLeaseholdContract(regionId, WORLD_ID);
Assertions.assertEquals(PLAYER_B, lease.tenantId(), "Tenant must still be on the lease");
Assertions.assertNotNull(lease.terminationEffectiveDate(), "Eviction must still be pending");

// ...so the region is still occupied and cannot be re-rented.
Assertions.assertInstanceOf(RealtyBackend.RentResult.AlreadyOccupied.class,
logic.rentRegion(regionId, WORLD_ID, PLAYER_B));
}

@Test
@DisplayName("cannot terminate a vacant lease")
void cannotTerminateVacant() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ sealed interface UnrentResult {
record Success(double refund, @NotNull String regionId,
@NotNull UUID landlordId) implements UnrentResult {}
record NoLeaseholdContract(@NotNull String regionId) implements UnrentResult {}
/** The lease is scheduled for termination and can only end on the effective date. */
record Terminating(@NotNull String regionId) implements UnrentResult {}
record RefundFailed(@NotNull String error) implements UnrentResult {}
record UpdateFailed(@NotNull String regionId) implements UnrentResult {}
record Error(@NotNull String message) implements UnrentResult {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,9 @@ public void setSafeBlockPredicate(@NotNull Predicate<Block> predicate) {
case RealtyBackend.UnrentResult.NoLeaseholdContract ignored ->
CompletableFuture.completedFuture(
(UnrentResult) new UnrentResult.NoLeaseholdContract(regionId));
case RealtyBackend.UnrentResult.Terminating ignored ->
CompletableFuture.completedFuture(
(UnrentResult) new UnrentResult.Terminating(regionId));
case RealtyBackend.UnrentResult.UpdateFailed ignored ->
CompletableFuture.completedFuture(
(UnrentResult) new UnrentResult.UpdateFailed(regionId));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ private void execute(@NotNull CommandContext<Source> ctx) {
case RealtyPaperApi.UnrentResult.NoLeaseholdContract noContract ->
sender.sendMessage(messages.messageFor(MessageKeys.UNRENT_NO_LEASEHOLD_CONTRACT,
Placeholder.unparsed("region", noContract.regionId())));
case RealtyPaperApi.UnrentResult.Terminating terminating ->
sender.sendMessage(messages.messageFor(MessageKeys.UNRENT_TERMINATING,
Placeholder.unparsed("region", terminating.regionId())));
case RealtyPaperApi.UnrentResult.RefundFailed failed ->
sender.sendMessage(messages.messageFor(MessageKeys.UNRENT_REFUND_FAILED,
Placeholder.unparsed("error", failed.error())));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,7 @@ private MessageKeys() {}
public static final String UNRENT_SUCCESS = "unrent.success";
public static final String UNRENT_NO_LEASEHOLD_CONTRACT = "unrent.no-leasehold-contract";
public static final String UNRENT_NOT_TENANT = "unrent.not-tenant";
public static final String UNRENT_TERMINATING = "unrent.terminating";
public static final String UNRENT_UPDATE_FAILED = "unrent.update-failed";
public static final String UNRENT_REFUND_FAILED = "unrent.refund-failed";
public static final String UNRENT_ERROR = "unrent.error";
Expand Down
2 changes: 2 additions & 0 deletions realty-paper/src/main/resources/messages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,8 @@ unrent:
<green>$<refund>.</green>'
no-leasehold-contract: <prefix> Region <yellow><region></yellow> is not a leasehold.
not-tenant: <prefix> You are not the tenant of region <yellow><region></yellow>.
terminating: <prefix> Region <yellow><region></yellow> is scheduled for termination
and can no longer be unrented. It ends on the scheduled date.
update-failed: <prefix> Failed to unrent region <yellow><region>.</yellow>
refund-failed: '<prefix> Failed to process refund: <error>'
error: '<prefix> Failed to unrent region: <error>'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,21 @@ void noLeaseholdContract() {
Assertions.assertInstanceOf(RealtyPaperApi.UnrentResult.NoLeaseholdContract.class, result);
}

@Test
@DisplayName("returns Terminating and leaves the region alone while an eviction is pending")
void terminating() {
when(realtyApi.unrentRegion(REGION_ID, WORLD_ID, TENANT_ID))
.thenReturn(new RealtyBackend.UnrentResult.Terminating());

protectedRegion.getOwners().addPlayer(TENANT_ID);

RealtyPaperApi.UnrentResult result = api.unrent(wgRegion, TENANT_ID).join();

Assertions.assertInstanceOf(RealtyPaperApi.UnrentResult.Terminating.class, result);
Assertions.assertEquals(1, protectedRegion.getOwners().size());
verify(regionProfileService, never()).applyFlags(any(), any(), any());
}

@Test
@DisplayName("success clears owners and applies FOR_LEASE flags")
void success() {
Expand Down
Loading