- Shipped
- 4 Agosti 2026, 20:57 UTC
- Author
- Kamo
- Commit
- e9edcb1
REST surface behind /leads/opportunities/reserved and the Reserve Opportunity control in the lead header, plus the service that owns the invariants. Rights, enforced explicitly because this service runs permitAll with no @EnableMethodSecurity — a missing check is a missing control, not a default-deny: VIEW_OPPORTUNITIES every read CREATE_OPPORTUNITIES raising a request, and asking for more time on your own EDIT_OPPORTUNITIES approving, editing, closing, ruling on an extension DELETE_OPPORTUNITIES "Deny Opportunity", which the UI puts behind a confirmation Deny is terminal but not destructive: the reservation, its splits and its whole history stay readable on the Denied tab. That is the point of denying rather than deleting. Details worth keeping: - The lead-header endpoint is readable with CREATE alone, since the button has to know which state to render — but a caller without VIEW who is not the holding agent gets a narrowed projection. The commission splits and justification that VIEW exists to protect are not disclosed by the widened gate. Rebuilt as an allow-list, so a field added later cannot leak by omission. - The list masks contact and company on unassigned leads for members without VIEW_UNASSIGNED_LEADS, matching LeadController. Who claimed what is not the secret; the prospect is. - Extending is gated on being the agent who holds the reservation, with no god bypass: holding a reservation is an identity fact, not a right, and an extension is filed under the requester's name. - A lapsed reservation that gets extended restarts rather than resumes, so reservedUntil == reservedFrom + approvedDays keeps holding — update() re-derives the end from the start, and a stale start would silently erase the grant. - The commission-split pool is VIEW_LEADS holders plus grant-all owners/admins, identical to what the picker endpoint offers. Computing it from the rights table alone made the dropdown offer an owner the backend then rejected. - Expiry is a scheduled sweep so the tabs can paginate on a real column, but every DTO reports daysRemaining against the database clock, so reads are correct between sweeps regardless. 36 tests cover the lifecycle, the date arithmetic and the split rules.