- Ya
- 11 Agosti 2026, 18:10 UTC
- Mwandishi
- Kamo
- Ahadi ya
- 3364634
A group can now gather people, buy a plan on its manager's own card, and hand its seats out. None of that existed: the only operations were create, approve and delete, so an owner who made a group got a label and no way to use it. BillingGroupSeatService is where money moves and entitlement is issued, kept apart from the record-keeping so no new route can be added that skips the gates. subscribe() refuses before create() is called rather than after, because create() opens a live Stripe subscription — an unapproved group, a legacy one, a manager with no card or a seat cap breach all stop there. Two defects made the group bill the wrong person: - The payer was whatever accountUid the request body named, unchecked, and the console sent the organization's. A group billed to the organization is not a billing group; it is a second name for the organization's own subscription. The payer is now derived from the manager and the two are asserted to agree before anything is charged. Sending accountUid is rejected outright. - ensureAccountForMember reuses the member's oldest AccountMember link, which for anyone ever added to the organization's account is the organization's account. ensurePersonalAccountForMember identifies by primaryMember instead, which is the only field that says an account belongs to a person. Where an owner manages their own group, "their card" and "the organization's card" are genuinely the same Account row and the same Stripe customer, and no code can tell them apart. That is refused unless confirmed, rather than papered over. Separately, two live bugs found while in here: - /self/seat charged the member and never issued them a seat. It called create() and returned; there was no SubscriptionMember row and no AccountLicense, so the buyer stayed unlicensed while their card was billed every month. - @Transactional in AccountBillingService had drifted onto a private method, so Spring ignored it and ensureAccountForMember ran with no transaction at all. A failure between its two saves orphaned an Account the next lookup could not see, and the following call created a second one. /self/coverage is new: it answers "who pays for my seat", which nothing did. It is also what stops someone already covered being offered a second seat on their own card.