You've already forked Arcturus-Morningstar-Extended
mirror of
https://github.com/duckietm/Arcturus-Morningstar-Extended.git
synced 2026-06-19 15:06:19 +00:00
feat(earnings): integrate native reward sources
This commit is contained in:
@@ -113,3 +113,15 @@ INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('earnings.club_job.item_id', '0', 'Items base id granted by club and job earnings claims.'),
|
||||
('earnings.club_job.item.quantity', '1', 'Furni quantity granted by club and job earnings claims.'),
|
||||
('earnings.club_job.hc.days', '0', 'HC days granted by club and job earnings claims.');
|
||||
|
||||
INSERT IGNORE INTO `emulator_settings` (`key`, `value`, `comment`) VALUES
|
||||
('earnings.daily_gift.native.enabled', '0', 'Use native hotel subsystem data for daily gift earnings claims when available.'),
|
||||
('earnings.games.native.enabled', '0', 'Use native hotel subsystem data for games earnings claims when available.'),
|
||||
('earnings.achievements.native.enabled', '0', 'Use native hotel subsystem data for achievements earnings claims when available.'),
|
||||
('earnings.marketplace.native.enabled', '1', 'Use marketplace sold item payouts for marketplace earnings claims.'),
|
||||
('earnings.hc_payday.native.enabled', '1', 'Use unclaimed HC payday logs for HC payday earnings claims.'),
|
||||
('earnings.level_progress.native.enabled', '0', 'Use native hotel subsystem data for level progress earnings claims when available.'),
|
||||
('earnings.donations.native.enabled', '0', 'Use native hotel subsystem data for donations earnings claims when available.'),
|
||||
('earnings.bonus_bag.native.enabled', '0', 'Use native hotel subsystem data for bonus bag earnings claims when available.'),
|
||||
('earnings.mystery_boxes.native.enabled', '0', 'Use native hotel subsystem data for mystery boxes earnings claims when available.'),
|
||||
('earnings.club_job.native.enabled', '0', 'Use native hotel subsystem data for club and job earnings claims when available.');
|
||||
|
||||
@@ -517,6 +517,7 @@ public final class Emulator {
|
||||
Emulator.config.register(prefix + "item_id", "0");
|
||||
Emulator.config.register(prefix + "item.quantity", "1");
|
||||
Emulator.config.register(prefix + "hc.days", "0");
|
||||
Emulator.config.register(prefix + "native.enabled", (category.equals("marketplace") || category.equals("hc_payday")) ? "1" : "0");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+201
-17
@@ -1,8 +1,10 @@
|
||||
package com.eu.habbo.habbohotel.earnings;
|
||||
|
||||
import com.eu.habbo.Emulator;
|
||||
import com.eu.habbo.habbohotel.catalog.marketplace.MarketPlace;
|
||||
import com.eu.habbo.habbohotel.users.Habbo;
|
||||
import com.eu.habbo.habbohotel.users.HabboBadge;
|
||||
import com.eu.habbo.habbohotel.users.subscriptions.SubscriptionHabboClub;
|
||||
import com.eu.habbo.messages.outgoing.users.AddUserBadgeComposer;
|
||||
|
||||
import java.sql.Connection;
|
||||
@@ -26,16 +28,22 @@ public class EarningsCenterManager {
|
||||
private final ConfigSource config;
|
||||
private final ClaimRepository claims;
|
||||
private final RewardApplier rewards;
|
||||
private final NativeIntegration nativeIntegration;
|
||||
private final Clock clock;
|
||||
|
||||
public EarningsCenterManager() {
|
||||
this(new EmulatorConfigSource(), new JdbcClaimRepository(), new HabboRewardApplier(), Clock.systemUTC());
|
||||
this(new EmulatorConfigSource(), new JdbcClaimRepository(), new HabboRewardApplier(), new DefaultNativeIntegration(), Clock.systemUTC());
|
||||
}
|
||||
|
||||
public EarningsCenterManager(ConfigSource config, ClaimRepository claims, RewardApplier rewards, Clock clock) {
|
||||
this(config, claims, rewards, new NoopNativeIntegration(), clock);
|
||||
}
|
||||
|
||||
public EarningsCenterManager(ConfigSource config, ClaimRepository claims, RewardApplier rewards, NativeIntegration nativeIntegration, Clock clock) {
|
||||
this.config = config;
|
||||
this.claims = claims;
|
||||
this.rewards = rewards;
|
||||
this.nativeIntegration = nativeIntegration;
|
||||
this.clock = clock;
|
||||
}
|
||||
|
||||
@@ -45,7 +53,7 @@ public class EarningsCenterManager {
|
||||
List<EarningsEntry> entries = new ArrayList<>();
|
||||
|
||||
for (EarningsCategory category : EarningsCategory.values()) {
|
||||
entries.add(buildEntry(userId, category, now));
|
||||
entries.add(buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
return entries;
|
||||
@@ -73,40 +81,68 @@ public class EarningsCenterManager {
|
||||
private EarningsClaimResult claim(Habbo habbo, EarningsCategory category) {
|
||||
int userId = getUserId(habbo);
|
||||
int now = now();
|
||||
CategoryDefinition definition = loadDefinition(category);
|
||||
CategoryDefinition definition = loadDefinition(habbo, category);
|
||||
|
||||
if (!definition.enabled()) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.DISABLED, buildEntry(userId, category, now));
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.DISABLED, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
if (this.nativeIntegration.handles(category) && nativeEnabled(category)) {
|
||||
return claimNative(habbo, userId, category, now, definition);
|
||||
}
|
||||
|
||||
if (definition.rewards().isEmpty()) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(userId, category, now));
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
String periodKey = periodKey(now, definition.cooldownSeconds());
|
||||
|
||||
try {
|
||||
if (!this.claims.recordClaim(userId, category.getKey(), periodKey, now)) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.ALREADY_CLAIMED, buildEntry(userId, category, now));
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.ALREADY_CLAIMED, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
this.rewards.grant(habbo, definition.rewards());
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.SUCCESS, buildEntry(userId, category, now));
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.SUCCESS, buildEntry(habbo, userId, category, now));
|
||||
} catch (SQLException e) {
|
||||
try {
|
||||
this.claims.removeClaim(userId, category.getKey(), periodKey);
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.ERROR, buildEntry(userId, category, now));
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.ERROR, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
}
|
||||
|
||||
private EarningsEntry buildEntry(int userId, EarningsCategory category, int now) {
|
||||
CategoryDefinition definition = loadDefinition(category);
|
||||
private EarningsClaimResult claimNative(Habbo habbo, int userId, EarningsCategory category, int now, CategoryDefinition definition) {
|
||||
try {
|
||||
if (definition.rewards().isEmpty() || !this.nativeIntegration.hasClaim(habbo, category)) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.NO_REWARD, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
|
||||
return this.nativeIntegration.claim(habbo, category)
|
||||
? new EarningsClaimResult(category, EarningsClaimResult.Status.SUCCESS, buildEntry(habbo, userId, category, now))
|
||||
: new EarningsClaimResult(category, EarningsClaimResult.Status.ERROR, buildEntry(habbo, userId, category, now));
|
||||
} catch (SQLException e) {
|
||||
return new EarningsClaimResult(category, EarningsClaimResult.Status.ERROR, buildEntry(habbo, userId, category, now));
|
||||
}
|
||||
}
|
||||
|
||||
private EarningsEntry buildEntry(Habbo habbo, int userId, EarningsCategory category, int now) {
|
||||
CategoryDefinition definition = loadDefinition(habbo, category);
|
||||
boolean claimable = false;
|
||||
int nextClaimAt = 0;
|
||||
|
||||
if (definition.enabled() && !definition.rewards().isEmpty()) {
|
||||
if (this.nativeIntegration.handles(category) && nativeEnabled(category)) {
|
||||
try {
|
||||
claimable = this.nativeIntegration.hasClaim(habbo, category);
|
||||
} catch (SQLException e) {
|
||||
claimable = false;
|
||||
}
|
||||
|
||||
return new EarningsEntry(category, true, claimable, 0, definition.rewards());
|
||||
}
|
||||
|
||||
String periodKey = periodKey(now, definition.cooldownSeconds());
|
||||
|
||||
try {
|
||||
@@ -121,7 +157,7 @@ public class EarningsCenterManager {
|
||||
return new EarningsEntry(category, definition.enabled(), claimable, nextClaimAt, definition.rewards());
|
||||
}
|
||||
|
||||
private CategoryDefinition loadDefinition(EarningsCategory category) {
|
||||
private CategoryDefinition loadDefinition(Habbo habbo, EarningsCategory category) {
|
||||
String key = CONFIG_PREFIX + category.getKey() + ".";
|
||||
boolean enabled = this.config.getBoolean(CONFIG_PREFIX + "enabled", false)
|
||||
&& this.config.getBoolean(key + "enabled", true);
|
||||
@@ -129,16 +165,27 @@ public class EarningsCenterManager {
|
||||
int pointsType = Math.max(0, this.config.getInt(key + "points.type", DEFAULT_POINTS_TYPE));
|
||||
List<EarningsReward> rewards = new ArrayList<>();
|
||||
|
||||
addReward(rewards, EarningsReward.TYPE_CREDITS, this.config.getInt(key + "credits", 0), 0);
|
||||
addReward(rewards, EarningsReward.TYPE_PIXELS, this.config.getInt(key + "pixels", 0), 0);
|
||||
addReward(rewards, EarningsReward.TYPE_POINTS, this.config.getInt(key + "points", 0), pointsType);
|
||||
addBadgeReward(rewards, this.config.getValue(key + "badge", ""));
|
||||
addItemReward(rewards, this.config.getInt(key + "item_id", 0), this.config.getInt(key + "item.quantity", 1));
|
||||
addHcReward(rewards, this.config.getInt(key + "hc.days", 0));
|
||||
if (nativeEnabled(category) && this.nativeIntegration.handles(category)) {
|
||||
try {
|
||||
rewards.addAll(this.nativeIntegration.rewards(habbo, category));
|
||||
} catch (SQLException ignored) {
|
||||
}
|
||||
} else {
|
||||
addReward(rewards, EarningsReward.TYPE_CREDITS, this.config.getInt(key + "credits", 0), 0);
|
||||
addReward(rewards, EarningsReward.TYPE_PIXELS, this.config.getInt(key + "pixels", 0), 0);
|
||||
addReward(rewards, EarningsReward.TYPE_POINTS, this.config.getInt(key + "points", 0), pointsType);
|
||||
addBadgeReward(rewards, this.config.getValue(key + "badge", ""));
|
||||
addItemReward(rewards, this.config.getInt(key + "item_id", 0), this.config.getInt(key + "item.quantity", 1));
|
||||
addHcReward(rewards, this.config.getInt(key + "hc.days", 0));
|
||||
}
|
||||
|
||||
return new CategoryDefinition(enabled, cooldown, rewards);
|
||||
}
|
||||
|
||||
private boolean nativeEnabled(EarningsCategory category) {
|
||||
return this.config.getBoolean(CONFIG_PREFIX + category.getKey() + ".native.enabled", true);
|
||||
}
|
||||
|
||||
private void addReward(List<EarningsReward> rewards, String type, int amount, int pointsType) {
|
||||
int clampedAmount = Math.min(Math.max(0, amount), MAX_CONFIGURED_REWARD);
|
||||
if (clampedAmount > 0) {
|
||||
@@ -213,6 +260,16 @@ public class EarningsCenterManager {
|
||||
void grant(Habbo habbo, List<EarningsReward> rewards) throws SQLException;
|
||||
}
|
||||
|
||||
public interface NativeIntegration {
|
||||
boolean handles(EarningsCategory category);
|
||||
|
||||
boolean hasClaim(Habbo habbo, EarningsCategory category) throws SQLException;
|
||||
|
||||
List<EarningsReward> rewards(Habbo habbo, EarningsCategory category) throws SQLException;
|
||||
|
||||
boolean claim(Habbo habbo, EarningsCategory category) throws SQLException;
|
||||
}
|
||||
|
||||
private static class EmulatorConfigSource implements ConfigSource {
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defaultValue) {
|
||||
@@ -344,4 +401,131 @@ public class EarningsCenterManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoopNativeIntegration implements NativeIntegration {
|
||||
@Override
|
||||
public boolean handles(EarningsCategory category) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasClaim(Habbo habbo, EarningsCategory category) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EarningsReward> rewards(Habbo habbo, EarningsCategory category) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean claim(Habbo habbo, EarningsCategory category) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static class DefaultNativeIntegration implements NativeIntegration {
|
||||
@Override
|
||||
public boolean handles(EarningsCategory category) {
|
||||
return category == EarningsCategory.MARKETPLACE || category == EarningsCategory.HC_PAYDAY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasClaim(Habbo habbo, EarningsCategory category) throws SQLException {
|
||||
return !rewards(habbo, category).isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EarningsReward> rewards(Habbo habbo, EarningsCategory category) throws SQLException {
|
||||
if (habbo == null) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.MARKETPLACE) {
|
||||
int soldPriceTotal = habbo.getInventory().getSoldPriceTotal();
|
||||
if (soldPriceTotal <= 0) {
|
||||
return List.of();
|
||||
}
|
||||
|
||||
if (MarketPlace.MARKETPLACE_CURRENCY == 0) {
|
||||
return List.of(new EarningsReward(EarningsReward.TYPE_CREDITS, soldPriceTotal, 0));
|
||||
}
|
||||
|
||||
return List.of(new EarningsReward(EarningsReward.TYPE_POINTS, soldPriceTotal, MarketPlace.MARKETPLACE_CURRENCY));
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.HC_PAYDAY) {
|
||||
return hcPaydayRewards(habbo);
|
||||
}
|
||||
|
||||
return List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean claim(Habbo habbo, EarningsCategory category) throws SQLException {
|
||||
if (habbo == null || habbo.getClient() == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.MARKETPLACE) {
|
||||
if (habbo.getInventory().getSoldPriceTotal() <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MarketPlace.getCredits(habbo.getClient());
|
||||
return true;
|
||||
}
|
||||
|
||||
if (category == EarningsCategory.HC_PAYDAY) {
|
||||
if (hcPaydayRewards(habbo).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SubscriptionHabboClub.processUnclaimed(habbo);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private List<EarningsReward> hcPaydayRewards(Habbo habbo) throws SQLException {
|
||||
List<EarningsReward> rewards = new ArrayList<>();
|
||||
|
||||
try (Connection connection = Emulator.getDatabase().getDataSource().getConnection();
|
||||
PreparedStatement statement = connection.prepareStatement("SELECT currency, SUM(total_payout) AS amount FROM logs_hc_payday WHERE user_id = ? AND claimed = 0 GROUP BY currency")) {
|
||||
statement.setInt(1, habbo.getHabboInfo().getId());
|
||||
|
||||
try (ResultSet set = statement.executeQuery()) {
|
||||
while (set.next()) {
|
||||
EarningsReward reward = currencyReward(set.getString("currency"), set.getInt("amount"));
|
||||
if (reward != null) {
|
||||
rewards.add(reward);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return rewards;
|
||||
}
|
||||
|
||||
private EarningsReward currencyReward(String currency, int amount) {
|
||||
if (amount <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
String normalized = currency == null ? "" : currency.trim().toLowerCase();
|
||||
return switch (normalized) {
|
||||
case "credits", "credit", "coins", "coin" -> new EarningsReward(EarningsReward.TYPE_CREDITS, amount, 0);
|
||||
case "duckets", "ducket", "pixels", "pixel" -> new EarningsReward(EarningsReward.TYPE_PIXELS, amount, 0);
|
||||
case "diamonds", "diamond" -> new EarningsReward(EarningsReward.TYPE_POINTS, amount, 5);
|
||||
default -> {
|
||||
try {
|
||||
yield new EarningsReward(EarningsReward.TYPE_POINTS, amount, Math.max(0, Integer.parseInt(normalized)));
|
||||
} catch (NumberFormatException e) {
|
||||
yield null;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+79
@@ -121,6 +121,44 @@ class EarningsCenterManagerTest {
|
||||
assertEquals(EarningsClaimResult.Status.SUCCESS, retried.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nativeMarketplaceRowsUseNativeClaimInsteadOfPeriodicClaimLedger() {
|
||||
TestConfig config = enabledConfig().with("earnings.marketplace.native.enabled", "1");
|
||||
TestClaims claims = new TestClaims();
|
||||
TestNativeIntegration nativeIntegration = new TestNativeIntegration(EarningsCategory.MARKETPLACE)
|
||||
.withReward(new EarningsReward(EarningsReward.TYPE_CREDITS, 45, 0));
|
||||
EarningsCenterManager manager = new EarningsCenterManager(config, claims, new TestRewards(), nativeIntegration, FIXED_CLOCK);
|
||||
|
||||
EarningsEntry entry = manager.getEntries(null).stream()
|
||||
.filter(current -> current.getCategory() == EarningsCategory.MARKETPLACE)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
EarningsClaimResult result = manager.claim(null, "marketplace");
|
||||
|
||||
assertTrue(entry.isClaimable());
|
||||
assertEquals(45, entry.getRewards().getFirst().getAmount());
|
||||
assertEquals(EarningsClaimResult.Status.SUCCESS, result.getStatus());
|
||||
assertEquals(1, nativeIntegration.claims);
|
||||
assertTrue(claims.claims.isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void nativeRowsWithoutAvailableRewardsAreNotClaimable() {
|
||||
TestConfig config = enabledConfig().with("earnings.hc_payday.native.enabled", "1");
|
||||
TestNativeIntegration nativeIntegration = new TestNativeIntegration(EarningsCategory.HC_PAYDAY);
|
||||
EarningsCenterManager manager = new EarningsCenterManager(config, new TestClaims(), new TestRewards(), nativeIntegration, FIXED_CLOCK);
|
||||
|
||||
EarningsEntry entry = manager.getEntries(null).stream()
|
||||
.filter(current -> current.getCategory() == EarningsCategory.HC_PAYDAY)
|
||||
.findFirst()
|
||||
.orElseThrow();
|
||||
EarningsClaimResult result = manager.claim(null, "hc_payday");
|
||||
|
||||
assertFalse(entry.isClaimable());
|
||||
assertEquals(EarningsClaimResult.Status.NO_REWARD, result.getStatus());
|
||||
assertEquals(0, nativeIntegration.claims);
|
||||
}
|
||||
|
||||
@Test
|
||||
void claimAllGrantsClaimableRowsAndSkipsAlreadyClaimedRows() throws SQLException {
|
||||
TestConfig config = enabledConfig()
|
||||
@@ -199,4 +237,45 @@ class EarningsCenterManagerTest {
|
||||
this.granted.addAll(rewards);
|
||||
}
|
||||
}
|
||||
|
||||
private static class TestNativeIntegration implements EarningsCenterManager.NativeIntegration {
|
||||
private final EarningsCategory category;
|
||||
private final List<EarningsReward> rewards = new ArrayList<>();
|
||||
private int claims = 0;
|
||||
|
||||
private TestNativeIntegration(EarningsCategory category) {
|
||||
this.category = category;
|
||||
}
|
||||
|
||||
private TestNativeIntegration withReward(EarningsReward reward) {
|
||||
this.rewards.add(reward);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean handles(EarningsCategory category) {
|
||||
return this.category == category;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasClaim(com.eu.habbo.habbohotel.users.Habbo habbo, EarningsCategory category) {
|
||||
return handles(category) && !this.rewards.isEmpty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<EarningsReward> rewards(com.eu.habbo.habbohotel.users.Habbo habbo, EarningsCategory category) {
|
||||
return handles(category) ? List.copyOf(this.rewards) : List.of();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean claim(com.eu.habbo.habbohotel.users.Habbo habbo, EarningsCategory category) {
|
||||
if (!hasClaim(habbo, category)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.claims++;
|
||||
this.rewards.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,6 +78,11 @@ This document is the emulator-side contract for the "Guadagni" UI.
|
||||
|
||||
For `points`, `pointsType` carries the currency type. For `badge`, `data` carries the badge code. For `item`, `data` carries the `items_base.id`. Other reward types keep `data` empty.
|
||||
|
||||
`marketplace` and `hc_payday` can be native rows. In native mode the amounts come from existing server state:
|
||||
|
||||
- `marketplace`: sold marketplace offers waiting for payout
|
||||
- `hc_payday`: unclaimed rows in `logs_hc_payday`
|
||||
|
||||
## Result Status
|
||||
|
||||
- `success`
|
||||
|
||||
@@ -63,8 +63,10 @@ Add emulator settings with safe defaults:
|
||||
- `earnings.<category>.item_id=0`
|
||||
- `earnings.<category>.item.quantity=1`
|
||||
- `earnings.<category>.hc.days=0`
|
||||
- `earnings.<category>.native.enabled=0/1`
|
||||
|
||||
The feature defaults off so existing hotels do not receive surprise economy changes after deploying the jar.
|
||||
Marketplace and HC payday default to native integrations once the feature is enabled, because both already have server-side claim ledgers.
|
||||
|
||||
## Packet Contract
|
||||
|
||||
@@ -90,6 +92,8 @@ Composer format is intentionally simple and renderer-friendly: category key, ena
|
||||
- Roll back the claim record if a DB-backed reward grant fails.
|
||||
- Use the database unique key to prevent concurrent double claims.
|
||||
- `claim all` processes only claimable rows and returns per-category results.
|
||||
- Marketplace claims use the existing marketplace sold-offer payout path.
|
||||
- HC payday claims use existing unclaimed `logs_hc_payday` rows.
|
||||
|
||||
## Tests
|
||||
|
||||
|
||||
Reference in New Issue
Block a user