Reserve, approve and audit lead opportunity claims

FeatureSecurityService
Shipped
August 4, 2026 at 8:57 PM 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.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing