You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-20 23:36:19 +00:00
fix(catalog): claim vouchers before rewards
Move voucher exhaustion checks and history persistence behind a synchronized per-voucher claim path. Rewards are now applied only after the history row is inserted successfully, preventing duplicate or failed-claim redemption from granting credits, points, or catalog items. Adds a contract test for claim ordering. Maven verification was attempted but blocked by sandbox network/plugin resolution after escalation usage was exhausted; diff --check passes.
This commit is contained in:
@@ -711,18 +711,22 @@ public class CatalogManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (voucher.isExhausted()) {
|
Voucher.ClaimResult claimResult = voucher.claimForUser(habbo.getHabboInfo().getId());
|
||||||
client.sendResponse(new RedeemVoucherErrorComposer(Emulator.getGameEnvironment().getCatalogManager().deleteVoucher(voucher) ? RedeemVoucherErrorComposer.INVALID_CODE : RedeemVoucherErrorComposer.TECHNICAL_ERROR));
|
switch (claimResult) {
|
||||||
return;
|
case CLAIMED:
|
||||||
|
break;
|
||||||
|
case EXHAUSTED:
|
||||||
|
client.sendResponse(new RedeemVoucherErrorComposer(Emulator.getGameEnvironment().getCatalogManager().deleteVoucher(voucher) ? RedeemVoucherErrorComposer.INVALID_CODE : RedeemVoucherErrorComposer.TECHNICAL_ERROR));
|
||||||
|
return;
|
||||||
|
case USER_LIMIT:
|
||||||
|
client.sendResponse(new ModToolIssueHandledComposer("You have exceeded the limit for redeeming this voucher."));
|
||||||
|
return;
|
||||||
|
case FAILED:
|
||||||
|
default:
|
||||||
|
client.sendResponse(new RedeemVoucherErrorComposer(RedeemVoucherErrorComposer.TECHNICAL_ERROR));
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (voucher.hasUserExhausted(habbo.getHabboInfo().getId())) {
|
|
||||||
client.sendResponse(new ModToolIssueHandledComposer("You have exceeded the limit for redeeming this voucher."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
voucher.addHistoryEntry(habbo.getHabboInfo().getId());
|
|
||||||
|
|
||||||
if (voucher.points > 0) {
|
if (voucher.points > 0) {
|
||||||
client.getHabbo().givePoints(voucher.pointsType, voucher.points);
|
client.getHabbo().givePoints(voucher.pointsType, voucher.points);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ import java.util.List;
|
|||||||
public class Voucher {
|
public class Voucher {
|
||||||
private static final Logger LOGGER = LoggerFactory.getLogger(Voucher.class);
|
private static final Logger LOGGER = LoggerFactory.getLogger(Voucher.class);
|
||||||
|
|
||||||
|
public enum ClaimResult {
|
||||||
|
CLAIMED,
|
||||||
|
EXHAUSTED,
|
||||||
|
USER_LIMIT,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
||||||
public final int id;
|
public final int id;
|
||||||
public final String code;
|
public final String code;
|
||||||
public final int credits;
|
public final int credits;
|
||||||
@@ -58,18 +65,34 @@ public class Voucher {
|
|||||||
return this.amount > 0 && this.history.size() >= this.amount;
|
return this.amount > 0 && this.history.size() >= this.amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void addHistoryEntry(int userId) {
|
public synchronized ClaimResult claimForUser(int userId) {
|
||||||
int timestamp = Emulator.getIntUnixTimestamp();
|
if (this.isExhausted()) {
|
||||||
this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp));
|
return ClaimResult.EXHAUSTED;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.hasUserExhausted(userId)) {
|
||||||
|
return ClaimResult.USER_LIMIT;
|
||||||
|
}
|
||||||
|
|
||||||
|
int timestamp = Emulator.getIntUnixTimestamp();
|
||||||
|
if (!this.insertHistoryEntry(userId, timestamp)) {
|
||||||
|
return ClaimResult.FAILED;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp));
|
||||||
|
return ClaimResult.CLAIMED;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean insertHistoryEntry(int userId, int timestamp) {
|
||||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO voucher_history (`voucher_id`, `user_id`, `timestamp`) VALUES (?, ?, ?)")) {
|
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection(); PreparedStatement statement = connection.prepareStatement("INSERT INTO voucher_history (`voucher_id`, `user_id`, `timestamp`) VALUES (?, ?, ?)")) {
|
||||||
statement.setInt(1, this.id);
|
statement.setInt(1, this.id);
|
||||||
statement.setInt(2, userId);
|
statement.setInt(2, userId);
|
||||||
statement.setInt(3, timestamp);
|
statement.setInt(3, timestamp);
|
||||||
|
|
||||||
statement.execute();
|
return statement.executeUpdate() > 0;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
LOGGER.error("Caught SQL exception", e);
|
LOGGER.error("Caught SQL exception", e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.eu.habbo.habbohotel.catalog;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
class VoucherClaimContractTest {
|
||||||
|
private static String voucherSource() throws Exception {
|
||||||
|
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/Voucher.java"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String catalogManagerSource() throws Exception {
|
||||||
|
return Files.readString(Path.of("src/main/java/com/eu/habbo/habbohotel/catalog/CatalogManager.java"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void voucherClaimIsSynchronizedAndPersistsBeforeRewardEligibility() throws Exception {
|
||||||
|
String source = voucherSource();
|
||||||
|
|
||||||
|
assertTrue(source.contains("public synchronized ClaimResult claimForUser(int userId)"),
|
||||||
|
"voucher claim should check limits and persist history under a per-voucher lock");
|
||||||
|
assertTrue(source.contains("private boolean insertHistoryEntry"),
|
||||||
|
"history insert should report database failure to the caller");
|
||||||
|
|
||||||
|
int insertCall = source.indexOf("insertHistoryEntry(userId, timestamp)");
|
||||||
|
int memoryAppend = source.indexOf("this.history.add(new VoucherHistoryEntry(this.id, userId, timestamp))");
|
||||||
|
|
||||||
|
assertTrue(insertCall > -1, "claimForUser must persist the history row");
|
||||||
|
assertTrue(memoryAppend > insertCall,
|
||||||
|
"in-memory history must only be updated after the database insert succeeds");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void catalogRewardsOnlyAfterVoucherClaimSucceeds() throws Exception {
|
||||||
|
String source = catalogManagerSource();
|
||||||
|
|
||||||
|
int claim = source.indexOf("Voucher.ClaimResult claimResult = voucher.claimForUser");
|
||||||
|
int claimedGuard = source.indexOf("case CLAIMED", claim);
|
||||||
|
int pointsGrant = source.indexOf("client.getHabbo().givePoints", claim);
|
||||||
|
int creditsGrant = source.indexOf("client.getHabbo().giveCredits", claim);
|
||||||
|
|
||||||
|
assertTrue(claim > -1, "CatalogManager must claim the voucher before applying rewards");
|
||||||
|
assertTrue(claimedGuard > claim, "voucher rewards should only continue for a CLAIMED result");
|
||||||
|
assertTrue(pointsGrant > claimedGuard, "points must be granted only after CLAIMED");
|
||||||
|
assertTrue(creditsGrant > claimedGuard, "credits must be granted only after CLAIMED");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user