fix(store): prevent double refund on concurrent cancel
The cancel flow credited the buyer before flipping the message body,
so two admins clicking at the same time could each pass the marker
check and each trigger addMoney, doubling the refund.
Flip the message body first with a conditional UPDATE on the original
body and bail out when affectedRows is 0. Only the request that wins
the UPDATE proceeds to credit the buyer and write the refund log.
Roll back the body if addMoney returns false.
feat(store): manual delivery with admin workflow in IM
Discord is a notification channel, not a delivery method. Split the
concern: GiveType::Manual replaces GiveType::Discord as the delivery
type, ManualGive settles the purchase (money, stock, log) at buy time,
and delivery is performed by an admin in-game.
PurchaseDialogNotifier posts new manual orders to the shared admin
dialog: the bot is the author, admins are members, the message shows
the server, item x count, a link to the buyer profile, and the
command in a code block.
Three inline actions per order:
- Copy 14 client-side, clipboard
- I delivered 14 atomic UPDATE on the message body, first click wins
- Cancel and refund 14 credits the buyer via memberData and writes a
buy_store_refund row through PurchaseLog::writeRefund
Admin membership in the server dialog is synced on each purchase:
new owners are added, removed owners are dropped, the bot stays first.
StorePurchaseService notifies admins only for Manual. StoreApiController
and StoreAdminDeleteService use UrlGeneratorInterface for URLs instead
of string concatenation. Rename name -> serverName in
StoreProductQuery::findForPurchase and GiveContext.
Discord webhook (discord_hook_shop) is used only as an additional
notification channel for manual orders, in plain text.
Migration 19248:
- products: give 'discord' -> 'manual'
- bundles: '"give":"discord"' -> '"give":"manual"' in command JSON
- new translations for admin workflow and player refund notice
- remove obsolete ADMIN_PURCHASE_NOTIFY_SUBJECT(_GLOBAL)
fix(store): route GiveQueue to RCON, classify placeholder failures, read member fields from main db
GiveType::fromGive mapped GiveQueue to Inventory, so store purchases went
through the wrong delivery path. Placeholder failures were indistinguishable,
surfacing server unavailable for missing Steam. Member fields were read
through the money connection without cache. ValidatePayment duplicated the
identity check.
- GiveType: GiveQueue !92 Rcon
- PlaceholderResolver: PlaceholderKind (Identity/Presence/Runtime/Unknown)
and a single missingIdentity() preflight
- RconGive: match on PlaceholderKind, no preflight duplication
- StorePurchaseService: take CurrentUser, drop flat fields and ValidatePayment
- MemberDataFactory/memberData: split $mainDb from $moneyDb
- CurrentUser::getServerAuth()
- Frontend: SteamID as placeholder, not value
fix(admin): layout alignment and Response support
- restore text-right on #naviButtons for legacy float-based toolbars
- wrap cron navi in d-inline-flex to prevent button collapse
- capture controller return value in AdminRunner and pass Response
through, matching FrontRunner behaviour
feat(store): distinguish missing identity from offline player
When the account has neither a Steam id nor a server auth id, the
store cannot identify the player in the game at all, but RconGive
returned playerOffline and the UI asked the user to join the server.
Add GiveResult::identityMissing, return it when both steam and
playerServerAuth are empty, and surface STORE_IDENTITY_MISSING from
the purchase match. playerOffline is now returned only when an
identifier exists but the player is not on the server.
fix(store): use Steam auth id and drop undefined from purchase POST
StorePurchaseService read $_SESSION[steam_id] (linked profile) instead
of $_SESSION[STEAMID] (Steam auth), so userIsOnline() missed players
who were online.
form_submit_ajax unconditionally read #username_inp, #steam_inp and
#server_auth; .val() returned undefined for absent fields and the
literal undefined went into every purchase request.
Prefer STEAMID when building GiveContext, coalesce the three lookups and ignore the literal undefined on the server side.
fix(store/give): fail explicitly on unresolved placeholders
PlaceholderResolver previously treated empty placeholder values
null as absent and let them slip through to str_ireplace,
so commands like tele %crossid% ... reached the game server with
literal tokens and the game replied player not found.
- add PlaceholderNotResolvedException with the missing placeholder list
- collect unresolved placeholders before substitution and throw
instead of emitting a half-built command
- catch it separately in RconGive and return GiveResult::notDeliverable
instead of misleading serverOffline
- persist the failure via PurchaseLog::writePlaceholderFailure