I put the transaction on the controllers. The controller knows about the larger framework since it probably has at least metadata like annotations of the framework.
As to the unit of work, it’s a good idea. You can have each use case start a transaction. Internally the unit of work either starts the actual transaction or increases a counter of invoked starts. Each use case would then call commit or reject. When the commit count equals 0, invoke the actual commit. Reject skips all of that, rolls back, then errors out (exception or return code).
In your example the wrapping use case calls start (c=1), the place order calls start(c=2), place order commits (c=1), bonus calls start (c=2), bonus calls commit (c=1), wrapping commits (c=0) so actually commit.
I leave subtransactions to you.