Back to Feed
article 3m read

A seat check on every way into a team nearly broke plan changes

Custom plans resolved to nothing and nothing meant unlimited, so unknown codes now fall to the free tier. Invites, reactivations, transfers and first sign-in all ask the same question, and a copied existence check would have blocked every workspace edit.

Article DepthPractice

Flow

Where the room check runs

  1. 1Invite

    Always checks before the new person is saved.

  2. 2Bring back

    Checks only if the person had been removed.

  3. 3Move over

    Checks the new team only if the person is active and the team changes.

  4. 4First sign-in

    Checks after the new team is made, before the person is saved.

  5. 5Store create

    Checks the store cap inside the same transaction, before any write.

Every new plan I could make from my admin screen would have come with no member limit.

Earlier that day I'd made plans editable from the admin screen of my scheduling app. You could make a tier and set how many stores and people a workspace on it gets. A few hours later I hooked those limits up to the places where people join a workspace. Part of that was fixing how a plan gets looked up.

Why an unknown plan meant no limit

The lookup only knew the built-in tiers. Any other code came back empty, and that meant every custom plan I could now make. The new member check treats an empty limit as "no limit." That's right for a tier with no cap. It's wrong for a tier the lookup just couldn't find.

So I changed the order. A saved plan with that exact code wins. If there isn't one, the built-in plan for that code is used. If there's no built-in either, the free tier is used. An unknown code now gets the free tier's limits, not none. Two tests hand in a made-up code and check that the lookups happen in that order.

Four ways onto a team

The invite form is the obvious way in. But an admin can also bring back someone who was removed, or move a person over from another workspace. And signing in for the first time makes an account right there. The middle two are updates, not new rows, so they're easy to miss.

All four now ask the same question: is there room for one more? The admin paths only ask when the change really adds someone.

Bringing a removed member back counts as a new seat, so that path checks for room first.

Each door has a test that sets the count at the limit and checks that nothing gets written. Sign-in is tested on only one of its two paths.

The sign-in door is mostly for show right now. Both sign-in paths make a brand-new workspace first, so it's empty when the check runs. If it ever did say no, that empty workspace would be left behind.

The check that asked the wrong table

To bring someone back or move them, those handlers first load the person. That same block got copied into the handler that edits a workspace, where it looked up a person by the workspace's ID. No person has that ID, so every workspace edit there would have failed, plan changes included. It compiled and read fine. A follow-up commit a few minutes later fixed the lookup and added two tests for it.

Stores get their own check

Making a store now checks the store limit first, inside the same transaction, before anything is saved. A test puts a workspace on a plan with no stores allowed and checks that nothing got written. The store check and the member check throw different kinds of errors, which I didn't sort out here.

Two invites can still take the last seat

The tests use stubs, so they show the calls happen in order, not that the real count is right. The check counts first and saves second, so two invites at the exact same moment could both grab the last seat. Any workspace whose plan code matches no saved or built-in plan now gets the free tier's limits. If one of those is already over, it keeps everyone it has but can't add a single person, not even by bringing someone back.

Keep reading

Pick up a connected idea or branch into a nearby one.

5 paths forward
Continue the thread

Paying customers matched by email, even the addresses the billing code made up itself

The lookup that decided whether an organization already had a payment customer searched by an email my code sometimes guessed or generated, and could hand one org's billing record to another. I moved identity onto the identity provider's org ID and tied retries to it, but the same commit left an ignored retry flag, a delete check that reads error wording, a gutted seat counter, and no tests.

via #billing · #multi-tenancy · #api-design