Bulk-create members and team members, one transaction per record

FeatureSecurityService
Shipped
29 Agosti 2026, 20:16 UTC
Author
Kamo
Commit
0e87cad

The two bulk-add screens need to create a spreadsheet of people in one submission and report on each of them individually. That could not be built on the existing endpoints, so the creation pipeline moves out of the controller. MemberCreationService now owns it, marked REQUIRES_NEW. Each record therefore commits or rolls back on its own, which is the whole premise of the screen: a failure at record 7 leaves 1-6 created, and only the rows that failed stay on the page to be corrected. Had the loop stayed in the controller, Spring's proxy would not have seen the call (self-invocation), every record would have shared one transaction, and the first caught failure would have marked it rollback-only and discarded the members that had already succeeded at commit time. MemberBulkCreator runs the batch and is deliberately NOT transactional for the same reason -- an outer transaction would be poisoned by exactly the exceptions it exists to absorb. It also refuses an email or username repeated within one submission, which would otherwise surface as "already a member" and send the admin looking for a member this very request had created a second earlier, and it releases those claims again when a row fails for some other reason. Both single-record endpoints now delegate to the same service, so the bulk path runs an identical sequence: reuse the User matching the personal email (case-insensitive, oldest wins), refuse a duplicate membership with 409, create through MemberService, mirror into the securityProvider org, re-materialize rights, then send the verification and WELCOME emails. Two other changes fall out of this: MemberUsernameGenerator fills in a blank username, in the order the product asked for -- first.last, f.last, first.l, first.m.last, then the same four numbered. It folds accents, respects the 20-character column, never ends on a separator and never mints a reserved handle. Availability is probed globally, because login resolves LOWER(u.username) across the whole table before it looks at any org. UsernameAvailabilityRepository is SecurityService-local so this ships without a shared-library version bump; the shared finder is exact-case and throws outright once two rows do collide. A hand-typed username that collides only by case is now refused rather than creating a second user. That pair is unrecoverable: both accounts become ambiguous at the sign-in form and findByUsername throws for each of them thereafter. The single-record form always sends a username it has already validated, so nothing regresses there. **************** learns extractOrgId, which is extractUserId's sibling and was missing only because the two controllers using it predate the pattern. Four baseline entries go with it.

All changes

Je, unaona nini kuhusu usafiri?

Kila moja ya hizi updates ardhi katika nafasi yako ya kazi moja kwa moja. Kuanza bure na kuangalia kukua wiki baada ya wiki.

Kuwa Huru MileleMtazamo wa bei