This is exactly the sort of thing transactions are for.
User see an order in one tab. Gets distracted by email. Opens new tab. Sees order again. Pays for it. Closes some tabs. Sees the old tab with the order unpaid. Tries to pay for it again. See's error indicating that they've already paid.
Multiple users. Multiple tabs. Whatever the cause, transactions are what you use to ensure a change of state really happened. Money moving and orders going from paid to unpaid is a classic example.
The transaction has to guarantee that the test and set parts happen atomically. It guarantees that all things that must be set together are set together. It rolls back any changes it started to make if anything fails.
You test that the order is unpaid and then set it to paid while disallowing anything else to pay for it between the test and the set. You test that the money exists and record that some of it has been spent. You don't let anything happen between the test and set. You undo all changes in the transaction if any part of it fails.
Many databases support transactions. In fact the need for this to happen reliability is one of the main things that gave rise to the popularity of databases. But if you can write safe asynchronous code yourself you can pull this off without a database.
The key is not letting anything come between the test and the set. A classic way of doing that is not testing first but setting first and producing an error if setting shouldn't have worked. But be warned, debugging such code is no small thing.