In src/bounties/bounties.service.ts, markMergedAndRelease's team branch is:
if (team && team.splits.length > 0) {
...
await this.escrowService.splitRelease(bounty.escrowId, recipients);
}
If team.splits.length === 0, this branch is silently skipped — no splitRelease call happens, no Payment rows are created, the escrow is never touched. Execution falls straight through to:
assertTransition(bounty.status, BountyStatus.PAID);
bounty.status = BountyStatus.PAID;
bounty.paidAt = new Date();
return this.bountyRepo.save(bounty);
The bounty is marked PAID regardless, with paidAt stamped, even though zero dollars were released and the escrow is still sitting LOCKED.
A team can legitimately reach zero splits via the CASCADE-on-user-delete path #58 already documents (every member of a team's account gets deleted, cascading their TeamMemberSplit rows away). Once that happens, any bounty assigned to that team that later merges will silently record itself as fully paid without paying anyone — the exact opposite of #46's "stuck and unpayable" failure mode; this one actively falsifies the ledger.
Fix: when team.splits.length === 0, throw rather than silently falling through to the PAID transition — there is no recipient to pay, so the bounty shouldn't advance past MERGED until the team is repaired or reassigned.
In src/bounties/bounties.service.ts,
markMergedAndRelease's team branch is:If
team.splits.length === 0, this branch is silently skipped — nosplitReleasecall happens, noPaymentrows are created, the escrow is never touched. Execution falls straight through to:The bounty is marked
PAIDregardless, withpaidAtstamped, even though zero dollars were released and the escrow is still sittingLOCKED.A team can legitimately reach zero splits via the CASCADE-on-user-delete path #58 already documents (every member of a team's account gets deleted, cascading their
TeamMemberSplitrows away). Once that happens, any bounty assigned to that team that later merges will silently record itself as fully paid without paying anyone — the exact opposite of #46's "stuck and unpayable" failure mode; this one actively falsifies the ledger.Fix: when
team.splits.length === 0, throw rather than silently falling through to the PAID transition — there is no recipient to pay, so the bounty shouldn't advance past MERGED until the team is repaired or reassigned.