{"version":3,"sources":["token-pending.mjs"],"sourcesContent":["// TOKENS ON THEIR WAY (the owner, Oct 12: \"speed up speed perception … As an example — the new tokens purchase process\").\n//\n// A purchase is paid on Stripe's page and credited by its webhook, in one ledger transaction that writes the purchase\n// record and the balance together (functions/lib/payments-ledger.mjs). The client never adds a token. What it can do is\n// KNOW WHAT IS COMING: the pack was picked here, so before the redirect the shop notes the checkout (its session id,\n// the pack's tokens, any gift) on this device, and on the way back it shows \"+1,100 pending\" at once instead of an\n// unnumbered \"Adding your tokens…\", confirms it when the purchase record says it was credited, and takes it off again,\n// saying why, if the payment failed or expired.\n//\n// PENDING TOKENS ARE NEVER SPENDABLE, AND NEVER LOOK IT. The balance the game spends from (myTokens) is the server's\n// number only. The pending amount is a separate marker beside it (\"500 · +1,100 pending\"), never added into it; and it\n// is hidden the moment the balance already shows the credit (the user record's snapshot can arrive before the\n// purchase record's), so the same tokens are never shown twice.\n//\n// Pure: no DOM, no Firestore. app.js wires it (buyPack, watchPurchase, renderTokens); tests/token-pending.test.mjs runs it.\n\nexport const PENDING_KEY = 'kubicor.pendingCheckout';\nexport const PENDING_TTL_MS = 24 * 3600 * 1000;   // a checkout session lasts a day at most (payments-core sessionParams)\nconst ID_RE = /^cs_[a-z]{4}_[A-Za-z0-9]{8,200}$/;\n\n// What the shop notes before sending the buyer to Stripe. `balance` is the balance then, so the way back can tell\n// whether the balance already includes the credit. Returns the record written, or null when there is nothing to note.\nexport function stashCheckout(store, { id, pack, tokens, gift = null, balance = null, at = Date.now() } = {}) {\n  if (!store || !ID_RE.test(String(id || '')) || !(Number(tokens) > 0)) return null;\n  const rec = { id: String(id), pack: String(pack || ''), tokens: Math.round(Number(tokens)), at: Number(at) || Date.now(),\n                balance: Number.isFinite(balance) ? balance : null,\n                gift: gift && (gift.mode === 'friend' || gift.mode === 'code') ? { mode: gift.mode, toIgn: gift.toIgn ? String(gift.toIgn).slice(0, 40) : null } : null };\n  try { store.setItem(PENDING_KEY, JSON.stringify(rec)); } catch (e) { return null; }\n  return rec;\n}\n\n// The note for this checkout, if this device made it and it is still fresh. Anything else (another id, a day old,\n// unreadable) is dropped, so a stale note can never put a number on screen.\nexport function pendingFor(store, id, now = Date.now()) {\n  if (!store) return null;\n  let rec = null;\n  try { rec = JSON.parse(store.getItem(PENDING_KEY) || 'null'); } catch (e) { rec = null; }\n  if (!rec || rec.id !== id || !(rec.tokens > 0) || !(now - rec.at >= 0 && now - rec.at < PENDING_TTL_MS)) {\n    if (rec && (rec.id === id || !(now - rec.at < PENDING_TTL_MS))) clearPending(store);\n    return null;\n  }\n  return rec;\n}\nexport function clearPending(store) { try { store && store.removeItem(PENDING_KEY); } catch (e) {} }\n\n// WHERE A PURCHASE HAS GOT TO, from its record as the ledger writes it. null while it is still on its way.\n//   done      credited to the buyer (creditedTokens > 0, no gift)\n//   gifted    a friend's gift delivered          coded / fellBack   a gift code made (fellBack: the friend had gone)\n//   failed    the payment failed, or the checkout expired: nothing was added\nexport function purchaseOutcome(p) {\n  if (!p) return null;\n  const g = p.gift;\n  if (g && p.paid === true && ((g.mode === 'friend' && p.creditedTokens > 0) || (g.mode === 'code' && g.code))) {\n    return { state: g.mode === 'friend' ? 'gifted' : g.fellBackFrom ? 'fellBack' : 'coded',\n             tokens: Number(p.tokens) || 0, toIgn: g.toIgn || g.fellBackIgn || '' };\n  }\n  if (!g && p.creditedTokens > 0) return { state: 'done', tokens: Number(p.creditedTokens) || 0 };\n  if (p.status === 'failed' || p.status === 'expired') return { state: 'failed', expired: p.status === 'expired' };\n  return null;\n}\nexport const STILL_COMING = ['waiting', 'slow'];\n\n// THE BALANCE LINE. `watch` is the shop's purchase watch ({ state, pending }); `balance` the server's number.\n// Returns { balance, pending }: `pending` is the marker's number, 0 when there is none to show. Only the buyer's own\n// tokens are ever pending on their balance (a gift goes to someone else), only while the purchase is still coming,\n// and not once the balance has already risen by them since the checkout was noted.\nexport function balanceView(balance, watch) {\n  const bal = Number.isFinite(balance) ? balance : 0;\n  const p = watch && watch.pending;\n  if (!p || p.gift || !STILL_COMING.includes(watch.state)) return { balance: bal, pending: 0 };\n  if (Number.isFinite(p.balance) && bal >= p.balance + p.tokens) return { balance: bal, pending: 0 };\n  return { balance: bal, pending: p.tokens };\n}\n\n// The words for each state, with the pending amount when this device knows it. `fmt` formats a token count.\nexport function pendingWords(watch, fmt = (n) => String(n)) {\n  const p = watch && watch.pending;\n  if (!watch) return '';\n  if (watch.state === 'waiting' || watch.state === 'slow') {\n    if (!p) return '';\n    if (p.gift && p.gift.mode === 'friend') return `+${fmt(p.tokens)} for ${p.gift.toIgn || 'your friend'}: sending your gift…`;\n    if (p.gift) return `Your gift code for ${fmt(p.tokens)} is being made…`;\n    return `+${fmt(p.tokens)} pending: confirming your payment…`;\n  }\n  if (watch.state === 'failed') {\n    const n = p && !p.gift ? ` The pending +${fmt(p.tokens)} was taken off.` : '';\n    return (watch.expired ? 'That checkout expired before it was paid, so nothing was added.' : 'That payment did not go through, so nothing was added.') + n;\n  }\n  return '';\n}\n"],"mappings":"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","names":[],"file":"token-pending.mjs"}