From 0f153716760b22a7f7f43fc7bdb2672d0eec538f Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 13 Jun 2026 02:07:01 +0200 Subject: [PATCH] fix(marketplace): only pay out claimed offers after detach MarketPlace.getCredits previously removed sold offers from memory and granted credits before knowing whether marketplace_items.user_id had been detached in the database. If that update failed, the same sold offer could be loaded as claimable again later. Make removeUser report success, keep the offer claimable on failure, and only grant credits after the database detach succeeds. --- .../catalog/marketplace/MarketPlace.java | 16 ++++---- .../MarketPlaceCreditClaimContractTest.java | 38 +++++++++++++++++++ 2 files changed, 47 insertions(+), 7 deletions(-) create mode 100644 Emulator/src/test/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceCreditClaimContractTest.java diff --git a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java index e7952892..b0646439 100644 --- a/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java +++ b/Emulator/src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java @@ -398,11 +398,12 @@ public class MarketPlace { synchronized (client.getHabbo().getInventory()) { for (MarketPlaceOffer offer : offers) { if (offer.getState().equals(MarketPlaceState.SOLD)) { - client.getHabbo().getInventory().removeMarketplaceOffer(offer); - credits += offer.getPrice(); - removeUser(offer); - offer.needsUpdate(true); - Emulator.getThreading().run(offer); + if (removeUser(offer)) { + client.getHabbo().getInventory().removeMarketplaceOffer(offer); + credits += offer.getPrice(); + offer.needsUpdate(true); + Emulator.getThreading().run(offer); + } } } } @@ -416,13 +417,14 @@ public class MarketPlace { } } - private static void removeUser(MarketPlaceOffer offer) { + private static boolean removeUser(MarketPlaceOffer offer) { try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("UPDATE marketplace_items SET user_id = ? WHERE id = ?")) { statement.setInt(1, -1); statement.setInt(2, offer.getOfferId()); - statement.execute(); + return statement.executeUpdate() > 0; } catch (SQLException e) { LOGGER.error("Caught SQL exception", e); + return false; } } diff --git a/Emulator/src/test/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceCreditClaimContractTest.java b/Emulator/src/test/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceCreditClaimContractTest.java new file mode 100644 index 00000000..9c0faa49 --- /dev/null +++ b/Emulator/src/test/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlaceCreditClaimContractTest.java @@ -0,0 +1,38 @@ +package com.eu.habbo.habbohotel.catalog.marketplace; + +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class MarketPlaceCreditClaimContractTest { + private static String marketPlaceSource() throws Exception { + return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/marketplace/MarketPlace.java")); + } + + @Test + void soldOfferIsDetachedBeforeCreditsAreGranted() throws Exception { + String source = marketPlaceSource(); + int getCreditsStart = source.indexOf("public static void getCredits"); + int removeUserCall = source.indexOf("removeUser(offer)", getCreditsStart); + int creditAccumulator = source.indexOf("credits += offer.getPrice()", getCreditsStart); + int inventoryRemoval = source.indexOf("removeMarketplaceOffer(offer)", getCreditsStart); + + assertTrue(getCreditsStart > -1, "MarketPlace.getCredits must exist"); + assertTrue(removeUserCall > -1, "Sold marketplace offers must be detached in the database"); + assertTrue(removeUserCall < creditAccumulator, + "Credits must not be granted until the sold offer is detached from the seller in the database"); + assertTrue(removeUserCall < inventoryRemoval, + "The in-memory sold offer should remain claimable if the database detach fails"); + } + + @Test + void detachFailureIsObservableByCaller() throws Exception { + String source = marketPlaceSource(); + + assertTrue(source.contains("private static boolean removeUser"), + "removeUser must report whether the marketplace ownership update succeeded"); + } +}