feat: parcel return logic (GH-69)#232
Conversation
…-delivery 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
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
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
…g them 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
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
There was a problem hiding this comment.
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.
Flipping a COLLECTED parcel's status back to SENT re-armed a delivery even though the content row still held the collection snapshot, letting an admin one-click the receiver into collecting the same items twice. AdminParcelService.changeStatus now rejects any status change while a parcel is COLLECTED, and AdminParcelEditGui no longer offers the toggle for one, so the click surfaces the new admin.statusLocked notice instead of a silent no-op. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
The post-commit block after markReturned succeeded relied on each async step's own .exceptionally for safety, but a synchronous throw inside it (e.g. the scheduler rejecting work during plugin disable) would propagate to the outer exceptionally, which refunds the fee and gives the deposited items back even though the parcel is already a live SENT shipment whose content row holds those same items — duplicating them. Wrap the post-commit statements in a try/catch that only logs, so no synchronous exception past the commit point can ever reach the refund/give-back recovery path. Pre-commit behavior (refund + give-back + cannotReturn on a failed markReturned) is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UWGThsFyex5LiEyN63L3Wu
Correctness hardening: - Make AdminParcelService.changeStatus a conditional UPDATE (ParcelRepository.updateIfStatus, WHERE status = expected) so a stale admin-GUI snapshot can no longer resurrect a parcel that concurrently became COLLECTED. - Upsert (not insert-if-absent) the collected_parcels row on collect, so a re-collect after a failed best-effort delete overwrites the stale collectedAt instead of shortening the new return window. Cleanup / UX: - ReturnWindowPurgeTask now also deletes the delivery row for a purged parcel, closing the leak left by a failed delete at ParcelSendTask.deliver time. - Move the returnFeeWithdrawn notice to after markReturned commits, so a lost race (fee refunded) no longer leaves the player thinking the fee was kept. - Reword insufficientFunds to be fee-neutral; it fires for both send and return fees. - Drop the dead result == null check in ReturnGui.show (getReturnableParcels never completes with null). Tests / docs / plumbing: - Add name/lore relaxed-flag coverage to ReturnItemEquivalenceTest. - Document the transitive-equivalence assumption ParcelReturnValidator's grouping relies on. - Switch new-code exception logging to LOGGER.log(Level, msg, throwable) so stack traces aren't dropped. - Move returnWindow onto GuiManager (alongside the existing allowCollectingFromAnyLocker pattern) instead of threading it through LockerGui/SendingGui/ItemStorageGui as a pass-through constructor param. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…vice withdrawPlayer/depositPlayer were called from inside async CompletableFuture continuations chained off repository lookups, so they executed on a DB-executor thread rather than the main thread Vault providers expect. Dispatch both through Scheduler.complete/run to hop back onto the primary thread before touching Economy. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
@claude review |
|
Claude finished @Jakubk15's task in 2m 21s —— View job Claude's Review
Prior Gemini findings — status
Own review of the return flowWent through
No new correctness issues found beyond what's listed above — nothing here blocks merging on my read. The remaining integration tests (blocked locally by the Docker port-relay conflict mentioned in the PR description) are the main outstanding item before merge, as called out by the author. |
Closes #69.
Players can return collected parcels by depositing the same items back at any locker. The returned parcel ships back to its original entry locker like a normal parcel.
How it works
COLLECTED(new status) instead of deleting it, and keeps its content row as the collection snapshot used to validate a later return.checkDurability,checkItemName,checkLore,checkEnchantments,checkNbt(alltrueby default = items must be identical; relaxing flags loosens individual attributes).5.0 / 12.5 / 25.0by default) is charged, bypassed byparcellockers.fee.bypass.SENT, and delivered with the normal priority-aware delay.parcel-return-window, default 7 days). A background task purges expired collected parcels (parcel + content + collected row) every 30 minutes.Correctness notes
markCollectedonly fromDELIVERED,markReturnedonly fromCOLLECTED), so double-collect, double-return, return-vs-purge and return-vs-admin-delete races resolve safely; covered by integration tests.ParcelSendTasknow aborts unless the parcel is stillSENT, preventing re-delivery duplication across restarts.ParcelReturnServiceis structurally isolated from the refund/give-back recovery path (including synchronous throws), so a committed return can never be undone into duplicated items.COLLECTEDparcel's status (it would otherwise re-arm delivery of already-handed-out items).InstantPersisterstores ISO-8601 strings, which don't order correctly lexicographically).Config additions
parcel-return-window,small/medium/large-parcel-return-fee,return-checks.*(5 flags), new GUI items/titles for the return list and deposit GUIs, and new notices (returned,cannotReturn,returnItemsMismatch,returnWindowExpired,returnFeeWithdrawn,admin.statusLocked).Testing
wslrelay.exevscom.docker.backendloopback binding), and the CI workflow currently runs onlyshadowJar, not./gradlew test. They should be run once (locally after awsl --shutdown, or by adding a test step to CI) before merge.runServersmoke test passed: plugin enables cleanly,collected_parcelstable created, no errors.Notes
fix/issue-222(fix/issue 222 #230, merged) because this feature reworks the same collect flow.🤖 Generated with Claude Code