Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
0f0fb6f
feat: add COLLECTED parcel status and guard ParcelSendTask against re…
Jakubk15 Jul 3, 2026
2638dec
feat: add CollectedParcel domain and collected_parcels repository
Jakubk15 Jul 3, 2026
f834069
fix: compare return-window expiry temporally, not lexicographically
Jakubk15 Jul 3, 2026
ef1af91
feat: add conditional collect/return status flips and returnable quer…
Jakubk15 Jul 3, 2026
6ddf92e
feat: add return window, fees, attribute-check flags and return notic…
Jakubk15 Jul 3, 2026
827c547
feat: add config-driven item equivalence for parcel returns
Jakubk15 Jul 3, 2026
bee53f5
feat: add multiset return-content validator
Jakubk15 Jul 3, 2026
264d5c3
feat: keep collected parcels for the return window instead of deletin…
Jakubk15 Jul 3, 2026
54596bd
feat: add parcel return service and ParcelReturnEvent
Jakubk15 Jul 3, 2026
f47e28a
fix: keep post-commit return failures away from the refund path
Jakubk15 Jul 3, 2026
ad20fff
feat: purge collected parcels after the return window expires
Jakubk15 Jul 3, 2026
3e45231
feat: add return button, return list GUI and deposit GUI (GH-69)
Jakubk15 Jul 3, 2026
2a9f44f
fix: refuse status changes on COLLECTED parcels (GH-69)
Jakubk15 Jul 4, 2026
9f97f09
fix: contain post-commit failures in ParcelReturnService.execute (GH-69)
Jakubk15 Jul 4, 2026
6d86d46
Add Graphify to CLAUDE.md
Jakubk15 Jul 4, 2026
fb79943
fix: address GH-233 follow-up cleanups from GH-69 review
Jakubk15 Jul 4, 2026
0131680
fix: run Vault economy calls on the primary thread in ParcelReturnSer…
Jakubk15 Jul 4, 2026
df06e9b
Merge branch 'master' into feat/parcel-return-gh-69
Jakubk15 Jul 4, 2026
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
10 changes: 10 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,13 @@ Each domain (`locker`, `parcel`, `content`, `delivery`, `itemstorage`, `user`, `
### Testing

Tests live in `src/test/java/`. Integration tests (e.g. `LockerRepositoryIntegrationTest`) extend `IntegrationTestSpec` and use Testcontainers (MySQL) to test repository implementations against a real database. `ParcelPageTest` is a unit test with no container dependency.

## graphify

This project has a knowledge graph at graphify-out/ with god nodes, community structure, and cross-file relationships.

Rules:
- For codebase questions, first run `graphify query "<question>"` when graphify-out/graph.json exists. Use `graphify path "<A>" "<B>"` for relationships and `graphify explain "<concept>"` for focused concepts. These return a scoped subgraph, usually much smaller than GRAPH_REPORT.md or raw grep output.
- If graphify-out/wiki/index.md exists, use it for broad navigation instead of raw source browsing.
- Read graphify-out/GRAPH_REPORT.md only for broad architecture review or when query/path/explain do not surface enough context.
- After modifying code, run `graphify update .` to keep the graph current (AST-only, no API cost).
32 changes: 31 additions & 1 deletion src/main/java/com/eternalcode/parcellockers/ParcelLockers.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
import com.eternalcode.parcellockers.parcel.service.ParcelService;
import com.eternalcode.parcellockers.parcel.service.ParcelServiceImpl;
import com.eternalcode.parcellockers.parcel.task.ParcelSendTask;
import com.eternalcode.parcellockers.returns.ParcelReturnService;
import com.eternalcode.parcellockers.returns.ParcelReturnValidator;
import com.eternalcode.parcellockers.returns.ReturnItemEquivalence;
import com.eternalcode.parcellockers.returns.repository.CollectedParcelRepositoryOrmLite;
import com.eternalcode.parcellockers.returns.task.ReturnWindowPurgeTask;
import com.eternalcode.parcellockers.updater.UpdaterService;
import com.eternalcode.parcellockers.user.UserManager;
import com.eternalcode.parcellockers.user.UserManagerImpl;
Expand Down Expand Up @@ -122,12 +127,14 @@ public void onEnable() {
DeliveryRepositoryOrmLite deliveryRepository = new DeliveryRepositoryOrmLite(databaseManager, scheduler);
ItemStorageRepository itemStorageRepository = new ItemStorageRepositoryOrmLite(databaseManager, scheduler);
UserRepository userRepository = new UserRepositoryOrmLite(databaseManager, scheduler);
CollectedParcelRepositoryOrmLite collectedParcelRepository = new CollectedParcelRepositoryOrmLite(databaseManager, scheduler);

// service and managers
ParcelService parcelService = new ParcelServiceImpl(
noticeService,
parcelRepository,
parcelContentRepository,
collectedParcelRepository,
scheduler,
config,
this.economy,
Expand All @@ -152,6 +159,27 @@ public void onEnable() {
noticeService
);

ParcelReturnValidator returnValidator = new ParcelReturnValidator(new ReturnItemEquivalence(config.settings.returnChecks));
ParcelReturnService parcelReturnService = new ParcelReturnService(
parcelService,
parcelContentManager,
collectedParcelRepository,
deliveryManager,
lockerManager,
returnValidator,
scheduler,
config,
noticeService,
this.economy,
server
);

scheduler.timerAsync(
new ReturnWindowPurgeTask(parcelService, collectedParcelRepository, deliveryManager, config),
Duration.ofSeconds(30),
Duration.ofMinutes(30)
);

// guis
TriumphGui.init(this);
GuiManager guiManager = new GuiManager(
Expand All @@ -162,7 +190,9 @@ public void onEnable() {
parcelDispatchService,
parcelContentManager,
deliveryManager,
config.settings.allowCollectingFromAnyLocker
config.settings.allowCollectingFromAnyLocker,
parcelReturnService,
config.settings.parcelReturnWindow
);

MainGui mainGUI = new MainGui(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,33 @@ public static class ParcelMessages extends OkaeriConfig {
.sound(SoundEventKeys.ENTITY_ITEM_BREAK)
.build();
public Notice insufficientFunds = Notice.builder()
.chat("&4✘ &cYou do not have enough funds to send this parcel! Required: &6${AMOUNT}&c.")
.chat("&4✘ &cYou do not have enough funds to cover this fee! Required: &6${AMOUNT}&c.")
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
.build();
public Notice feeWithdrawn = Notice.builder()
.chat("&2✔ &a${AMOUNT} has been withdrawn from your account to cover the parcel sending fee.")
.sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP)
.build();
public Notice returned = Notice.builder()
.chat("&2✔ &aParcel returned. It is on its way back to the sender.")
.sound(SoundEventKeys.ENTITY_PLAYER_LEVELUP)
.build();
public Notice cannotReturn = Notice.builder()
.chat("&4✘ &cThis parcel cannot be returned right now. Your items were given back.")
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
.build();
public Notice returnItemsMismatch = Notice.builder()
.chat("&4✘ &cThe deposited items do not match the original parcel contents!")
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
.build();
public Notice returnWindowExpired = Notice.builder()
.chat("&4✘ &cThe return window for this parcel has expired!")
.sound(SoundEventKeys.ENTITY_VILLAGER_NO)
.build();
public Notice returnFeeWithdrawn = Notice.builder()
.chat("&2✔ &a${AMOUNT} has been withdrawn from your account to cover the parcel return fee.")
.sound(SoundEventKeys.ENTITY_EXPERIENCE_ORB_PICKUP)
.build();

@Comment({"", "# The parcel info message." })
public Notice parcelInfoMessages = Notice.builder()
Expand Down Expand Up @@ -189,6 +209,7 @@ public static class AdminMessages extends OkaeriConfig {
public Notice teleportWorldMissing = Notice.chat("&4✘ &cThat locker's world is not loaded.");
public Notice sizeTooSmall = Notice.chat("&4✘ &cThe parcel's contents do not fit in that size.");
public Notice destinationFull = Notice.chat("&4✘ &cThat destination locker is full.");
public Notice statusLocked = Notice.chat("&4✘ &cA collected parcel's status cannot be changed; it can only be returned.");
public Notice contentsUpdated = Notice.chat("&2✔ &aParcel contents updated.");
public Notice priorityUpdated = Notice.chat("&2✔ &aPriority updated and delivery time adjusted.");
public Notice noPermission = Notice.chat("&4✘ &cYou do not have permission to do that.");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,25 @@ public static class Settings extends OkaeriConfig {

@Comment({"", "# Large parcel fee in in-game currency"})
public double largeParcelFee = 50.0;

@Comment({"", "# How long after collection a parcel can still be returned.", "# Expired collected parcels are purged periodically."})
public Duration parcelReturnWindow = Duration.ofDays(7);

@Comment({"", "# Small parcel return fee in in-game currency"})
public double smallParcelReturnFee = 5.0;

@Comment({"", "# Medium parcel return fee in in-game currency"})
public double mediumParcelReturnFee = 12.5;

@Comment({"", "# Large parcel return fee in in-game currency"})
public double largeParcelReturnFee = 25.0;

@Comment({
"",
"# Which item attributes must match the original parcel content when a player returns a parcel.",
"# Material types and total amounts must always match; each flag below relaxes one attribute when set to false."
})
public ReturnChecks returnChecks = new ReturnChecks();
}

public static class GuiSettings extends OkaeriConfig {
Expand Down Expand Up @@ -490,6 +509,66 @@ public static class GuiSettings extends OkaeriConfig {

@Comment({ "", "# The lore line showing when the parcel has arrived. Placeholders: {DATE} - arrival date" })
public String parcelArrivedLine = "&aArrived on: &2{DATE}";

@Comment({ "", "# The title of the parcel return GUI" })
public String parcelReturnGuiTitle = "&5Return parcels";

@Comment({ "", "# The title of the return deposit GUI" })
public String parcelReturnDepositGuiTitle = "&5Deposit the parcel items";

@Comment({ "", "# The item of the parcel locker return button" })
public ConfigItem parcelLockerReturnItem = new ConfigItem()
.name("&5↩ Return parcels")
.lore(List.of("&5» &dClick to return a collected parcel."))
.type(Material.HOPPER)
.glow(true);

@Comment({ "", "# The item of the parcel in the return GUI" })
public ConfigItem parcelReturnRowItem = new ConfigItem()
.name("&d{NAME}")
.lore(List.of(
"&6Sender: &e{SENDER}",
"&6Size: &e{SIZE}",
"&6Description: &e{DESCRIPTION}"
)
)
.type(Material.CHEST_MINECART);

@Comment({ "", "# The item displayed in the return GUI when there is nothing to return" })
public ConfigItem noReturnableParcelsItem = new ConfigItem()
.name("&4✘ &cNo returnable parcels")
.lore(List.of("&cYou don't have any parcels to return."))
.type(Material.STRUCTURE_VOID);

@Comment({ "", "# The item of the confirm return button" })
public ConfigItem confirmReturnItem = new ConfigItem()
.name("&2✔ &aConfirm return")
.lore(List.of("&2» &aDeposit the original items above, then click to return the parcel."))
.type(Material.GREEN_DYE);

@Comment({ "", "# The lore line showing how long the parcel can still be returned. Placeholder: {DURATION}" })
public String returnWindowRemainingLine = "&5Return window: &d{DURATION} left";

@Comment({ "", "# The lore line shown when the return window has expired." })
public String returnWindowExpiredLine = "&cReturn window expired";
}

public static class ReturnChecks extends OkaeriConfig {

@Comment("# Whether durability (damage) must match the original items.")
public boolean checkDurability = true;

@Comment("# Whether custom display names must match the original items.")
public boolean checkItemName = true;

@Comment("# Whether enchantments must match the original items.")
public boolean checkEnchantments = true;

@Comment("# Whether lore must match the original items.")
public boolean checkLore = true;

@Comment({"# Whether all remaining item data (NBT) must match the original items.", "# When false, only the attributes enabled above are compared."})
public boolean checkNbt = true;
}

public static class DiscordSettings extends OkaeriConfig {
Expand Down
28 changes: 27 additions & 1 deletion src/main/java/com/eternalcode/parcellockers/gui/GuiManager.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,13 @@
import com.eternalcode.parcellockers.parcel.Parcel;
import com.eternalcode.parcellockers.parcel.service.ParcelDispatchService;
import com.eternalcode.parcellockers.parcel.service.ParcelService;
import com.eternalcode.parcellockers.returns.CollectedParcel;
import com.eternalcode.parcellockers.returns.ParcelReturnService;
import com.eternalcode.parcellockers.shared.Page;
import com.eternalcode.parcellockers.shared.PageResult;
import com.eternalcode.parcellockers.user.User;
import com.eternalcode.parcellockers.user.UserManager;
import java.time.Duration;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
Expand All @@ -35,6 +38,8 @@ public class GuiManager {
private final ParcelContentManager parcelContentManager;
private final DeliveryManager deliveryManager;
private final boolean allowCollectingFromAnyLocker;
private final ParcelReturnService parcelReturnService;
private final Duration returnWindow;

public GuiManager(
ParcelService parcelService,
Expand All @@ -44,7 +49,9 @@ public GuiManager(
ParcelDispatchService parcelDispatchService,
ParcelContentManager parcelContentManager,
DeliveryManager deliveryManager,
boolean allowCollectingFromAnyLocker
boolean allowCollectingFromAnyLocker,
ParcelReturnService parcelReturnService,
Duration returnWindow
) {
this.parcelService = parcelService;
this.lockerManager = lockerManager;
Expand All @@ -54,6 +61,13 @@ public GuiManager(
this.parcelContentManager = parcelContentManager;
this.deliveryManager = deliveryManager;
this.allowCollectingFromAnyLocker = allowCollectingFromAnyLocker;
this.parcelReturnService = parcelReturnService;
this.returnWindow = returnWindow;
}

/** The configured return window, exposed so GUIs don't each need their own copy threaded in. */
public Duration returnWindow() {
return this.returnWindow;
}

/**
Expand Down Expand Up @@ -157,4 +171,16 @@ public CompletableFuture<Void> deleteAllParcels(CommandSender sender, NoticeServ
public CompletableFuture<Void> deleteAllLockers(CommandSender sender, NoticeService noticeService) {
return this.lockerManager.deleteAll(sender, noticeService);
}

public CompletableFuture<PageResult<Parcel>> getReturnableParcels(UUID receiver, Page page) {
return this.parcelService.getReturnable(receiver, page);
}

public CompletableFuture<Optional<CollectedParcel>> getCollectedInfo(UUID parcelId) {
return this.parcelReturnService.getCollectedInfo(parcelId);
}

public void returnParcel(Player player, Parcel parcel, List<ItemStack> deposited) {
this.parcelReturnService.returnParcel(player, parcel, deposited);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ private void notifyResult(Player player, EditResult result) {
case OK -> this.noticeService.create().notice(m -> m.admin.parcelUpdated).player(player.getUniqueId()).send();
case SIZE_TOO_SMALL -> this.noticeService.create().notice(m -> m.admin.sizeTooSmall).player(player.getUniqueId()).send();
case DESTINATION_FULL -> this.noticeService.create().notice(m -> m.admin.destinationFull).player(player.getUniqueId()).send();
case PARCEL_COLLECTED -> this.noticeService.create().notice(m -> m.admin.statusLocked).player(player.getUniqueId()).send();
}
}

Expand Down Expand Up @@ -227,6 +228,11 @@ private static ParcelSize nextSize(ParcelSize size) {
}

private static ParcelStatus nextStatus(ParcelStatus status) {
if (status == ParcelStatus.COLLECTED) {
// A COLLECTED parcel cannot be toggled back to SENT/DELIVERED; AdminParcelService
// rejects the change too, so this only avoids offering the flip in the first place.
return ParcelStatus.COLLECTED;
}
return status == ParcelStatus.SENT ? ParcelStatus.DELIVERED : ParcelStatus.SENT;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,16 @@ public void show(Player player, UUID entryLocker) {
entryLocker
);

ReturnGui returnGui = new ReturnGui(
this.guiSettings,
this.scheduler,
this.guiManager,
this.miniMessage,
this.noticeService
);

gui.setItem(21, this.guiSettings.parcelLockerCollectItem.toGuiItem(event -> collectionGui.show(player)));
gui.setItem(22, this.guiSettings.parcelLockerReturnItem.toGuiItem(event -> returnGui.show(player)));
gui.setItem(23, this.guiSettings.parcelLockerSendItem.toGuiItem(event -> new SendingGui(
this.scheduler,
this.guiSettings,
Expand Down
Loading
Loading