mischief: gogobee learns to fill orders placed on Pete's web board

The game-side half of Mischief Makers M3. gogobee polls Pete for storefront
orders and opens the contract itself — the money, the eligibility, the fight
are all its own, exactly as a Matrix !mischief buy.

- roster push now carries each buyer's advisory euro balance (keyed by localpart,
  a separate keyspace from the anonymous board token) and the live tier catalog,
  so the storefront renders gogobee's current prices and never hardcodes one
- placeWebMischief: the fulfilment path, debit-first-then-refund so the money
  state is a pure function of the order guid. DebitIdem + an order_guid stamp on
  the contract make a retried claim neither double-charge nor double-open
- resolveRosterToken recomputes the one-way board token per live player to name
  a mark; the buyer is @<username>:<homeserver> from Authentik's preferred_username
- a 30s poll loop drives it; a lost verdict just leaves the order pending to
  re-run, so the loop is its own retry and needs no durable queue
- euro.HasExternalTx lets the affordability gate tell a first attempt (no debt
  for a mischief buy, like Matrix) from a retry of one already paid

order_guid column added to mischief_contracts (schema + idempotent ALTER).
This commit is contained in:
prosolis
2026-07-14 21:12:37 -07:00
parent 6d402343e6
commit ba306f0b59
9 changed files with 646 additions and 12 deletions

View File

@@ -416,6 +416,21 @@ func (p *EuroPlugin) GetBalance(userID id.UserID) float64 {
return balance
}
// HasExternalTx reports whether a money move with this externalID has already
// been applied. It lets a retrying web caller tell a first attempt from a replay:
// on a first attempt the affordability gate must run, but on a retry of an order
// already debited the gate must be skipped — the debit is a settled fact, and
// re-reading the now-lower balance would wrongly bounce a hit the buyer paid for.
func (p *EuroPlugin) HasExternalTx(externalID string) bool {
if externalID == "" {
return false
}
var one int
err := db.Get().QueryRow(
`SELECT 1 FROM euro_transactions WHERE external_id = ? LIMIT 1`, externalID).Scan(&one)
return err == nil
}
// ---------------------------------------------------------------------------
// Idempotent money moves
//