Skip to content

Add parcel return feature#232

Draft
Jakubk15 wants to merge 12 commits into
masterfrom
feat/parcel-return-gh-69
Draft

Add parcel return feature#232
Jakubk15 wants to merge 12 commits into
masterfrom
feat/parcel-return-gh-69

Conversation

@Jakubk15

@Jakubk15 Jakubk15 commented Jul 3, 2026

Copy link
Copy Markdown
Member
  • feat: add COLLECTED parcel status and guard ParcelSendTask against re-delivery
  • feat: add CollectedParcel domain and collected_parcels repository
  • fix: compare return-window expiry temporally, not lexicographically
  • feat: add conditional collect/return status flips and returnable query to ParcelRepository
  • feat: add return window, fees, attribute-check flags and return notices to config
  • feat: add config-driven item equivalence for parcel returns
  • feat: add multiset return-content validator
  • feat: keep collected parcels for the return window instead of deleting them
  • feat: add parcel return service and ParcelReturnEvent
  • fix: keep post-commit return failures away from the refund path
  • feat: purge collected parcels after the return window expires
  • feat: add return button, return list GUI and deposit GUI (Parcel return logic #69)

Jakubk15 and others added 12 commits July 3, 2026 14:01
The collected_at column is a string-persisted ISO-8601 Instant; SQL range
operators compare it lexicographically and misorder same-second values.
Filter in Java and cover the cutoff boundary in the integration test.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
…y to ParcelRepository

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
…es to config

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
Orchestrates the return of a COLLECTED parcel: re-verifies status/receiver,
checks the return window, validates deposited items against the stored
content snapshot, checks entry-locker capacity, charges the return fee
(bypassable), flips the parcel into a reverse SENT shipment via
ParcelService.markReturned, and schedules the normal delivery task. Every
abort path hands the deposited items back and sends a specific notice; the
fee is refunded on any failure after it was charged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
After markReturned succeeds the parcel is a live shipment; a throwing
DeliveryManager.create (stale cached delivery from the original trip)
must not trigger the refund/give-back recovery, which would duplicate
the deposited items. Schedule the send task first and upsert the
delivery timestamp instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
Wires the last piece of the parcel return flow into the GUI layer:
GuiManager gains getReturnableParcels/getCollectedInfo/returnParcel
passthroughs to ParcelService/ParcelReturnService, ReturnGui lists a
player's collected parcels with a remaining-return-window lore line,
ReturnDepositGui lets them deposit the original items and confirms
the return (or gives everything back on close), and LockerGui gets a
"Return parcels" button between collect and send. Threads the return
window Duration through SendingGui/ItemStorageGui as well, since they
rebuild LockerGui/each other and would otherwise fail to compile
after LockerGui's constructor changed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a comprehensive parcel return system, including return validation, configurable item equivalence checks, database persistence for collected parcels, a periodic purge task for expired returns, and dedicated return GUIs. The code review highlights critical thread-safety issues with asynchronous Vault economy calls, potential NullPointerExceptions due to missing null/air checks in item validation, a potential ConcurrentModificationException when modifying item enchantments, and a performance concern regarding full table scans for expired parcels.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

if (!player.hasPermission(PARCEL_FEE_BYPASS_PERMISSION)) {
double fee = this.returnFeeFor(current.size());
if (fee > 0) {
boolean success = this.economy.withdrawPlayer(player, fee).transactionSuccess();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Thread Safety Issue: Asynchronous Vault Economy Call

Calling Vault's withdrawPlayer on an asynchronous thread is highly unsafe. Most economy providers (such as Essentials, CraftConomy, etc.) are not thread-safe and expect all economy transactions to be executed on the main server thread. Running this asynchronously can lead to race conditions, database deadlocks, or ConcurrentModificationExceptions.

Please wrap this economy withdrawal in a synchronous scheduler task to ensure it runs on the main thread.

}

private void refund(Player player, double fee) {
if (fee > 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Thread Safety Issue: Asynchronous Vault Economy Call

Similar to the withdrawal logic, calling depositPlayer on an asynchronous thread is unsafe because Vault economy providers are generally not thread-safe.

Please ensure that the refund operation is scheduled to run on the main server thread using this.scheduler.run(...).

Comment on lines +28 to +31
public boolean test(ItemStack expected, ItemStack actual) {
if (expected.getType() != actual.getType()) {
return false;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defensive Programming: Missing Null Checks

If either expected or actual is null, calling expected.getType() will throw a NullPointerException. Adding a defensive null check at the beginning of the method prevents potential crashes.

    @Override
    public boolean test(ItemStack expected, ItemStack actual) {
        if (expected == null || actual == null) {
            return expected == actual;
        }
        if (expected.getType() != actual.getType()) {
            return false;
        }

Comment on lines +21 to +46
public boolean matches(List<ItemStack> deposited, List<ItemStack> expected) {
List<ItemStack> samples = new ArrayList<>();
List<Integer> expectedTotals = new ArrayList<>();
List<Integer> depositedTotals = new ArrayList<>();

for (ItemStack item : expected) {
int index = this.indexOf(samples, item);
if (index < 0) {
samples.add(item);
expectedTotals.add(item.getAmount());
depositedTotals.add(0);
continue;
}
expectedTotals.set(index, expectedTotals.get(index) + item.getAmount());
}

for (ItemStack item : deposited) {
int index = this.indexOf(samples, item);
if (index < 0) {
return false;
}
depositedTotals.set(index, depositedTotals.get(index) + item.getAmount());
}

return expectedTotals.equals(depositedTotals);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Defensive Programming: Missing Null/Air Checks on List Elements

If the expected or deposited lists contain null or empty/air items, calling item.getAmount() will throw a NullPointerException. Adding a defensive check to skip null or air items in both loops ensures robustness.

    public boolean matches(List<ItemStack> deposited, List<ItemStack> expected) {
        List<ItemStack> samples = new ArrayList<>();
        List<Integer> expectedTotals = new ArrayList<>();
        List<Integer> depositedTotals = new ArrayList<>();

        for (ItemStack item : expected) {
            if (item == null || item.getType().isAir()) {
                continue;
            }
            int index = this.indexOf(samples, item);
            if (index < 0) {
                samples.add(item);
                expectedTotals.add(item.getAmount());
                depositedTotals.add(0);
                continue;
            }
            expectedTotals.set(index, expectedTotals.get(index) + item.getAmount());
        }

        for (ItemStack item : deposited) {
            if (item == null || item.getType().isAir()) {
                continue;
            }
            int index = this.indexOf(samples, item);
            if (index < 0) {
                return false;
            }
            depositedTotals.set(index, depositedTotals.get(index) + item.getAmount());
        }

        return expectedTotals.equals(depositedTotals);
    }

Comment on lines +61 to +63
if (!this.checks.checkEnchantments) {
meta.getEnchants().keySet().forEach(meta::removeEnchant);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Potential ConcurrentModificationException

Iterating over meta.getEnchants().keySet() and modifying the underlying ItemMeta via meta.removeEnchant inside the loop can throw a ConcurrentModificationException if the map returned by getEnchants() is a live view or if the implementation modifies the collection being iterated.

Copying the key set to a temporary list before iterating is a safer and more robust approach.

Suggested change
if (!this.checks.checkEnchantments) {
meta.getEnchants().keySet().forEach(meta::removeEnchant);
}
if (!this.checks.checkEnchantments) {
new ArrayList<>(meta.getEnchants().keySet()).forEach(meta::removeEnchant);
}

Comment on lines +36 to +48
public CompletableFuture<List<CollectedParcel>> findExpired(Instant cutoff) {
Objects.requireNonNull(cutoff, "Cutoff cannot be null");
// collected_at is persisted as an ISO-8601 string (InstantPersister); a SQL range operator
// would compare it lexicographically, which misorders same-second values with different
// fractional renderings. Compare temporally in Java instead — the table only holds parcels
// inside the return window, so a full scan per purge run is cheap.
return this.selectAll(CollectedParcelTable.class)
.thenApply(rows -> rows.stream()
.map(CollectedParcelTable::toCollectedParcel)
.filter(collected -> !collected.collectedAt().isAfter(cutoff))
.toList());
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Performance: Full Table Scan and In-Memory Filtering

Loading the entire collected_parcels table into memory and filtering it in Java is an anti-pattern that can degrade performance as the database grows.

While different fractional renderings of ISO-8601 strings can cause sub-second lexicographical misordering, a slight sub-second inaccuracy at the 7-day boundary is completely negligible for a purge task running every 30 minutes. Using a database-level WHERE clause with a range operator is much more efficient and scalable. Alternatively, storing the timestamp as epoch milliseconds (long) would allow perfect temporal filtering directly in SQL.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant