jonnify·codemojex /codemojex/commerce/the-keyshop

Codemojex · C6.2 · a dive of The Revenue Ledger and the KeyShop

Four rails, one order, exactly-once.

The KeyShop is the multi-rail pay-in that feeds the revenue ledger. A DB-stored PKG catalog sits behind one orders table with a rail discriminator; the money is pinned on the ORD at creation, and settlement is exactly-once on the OTX (rail, external_id) partial unique index. Telegram Stars is shipped end to end; the TON, USDT, and RUB verifiers are forward. The gross books to the same revenue_ledger.

Grounding — echo/…/codemojex/key_shop.ex (price_minor/3, create_order/3, settle_payment/1) · rails.ex (the frozen rail set + self_check!/0) · the PKG/ORD/OTX/WHK schemas · telegram_controller.ex · the keyshop story.

The rail pricer — one package, four minor units — select a rail
PKG · 100 keys stars_price 1449 usd face → 1884¢ edits touch later orders only stars · :star ×1 · 0 dp 1449 ton · :nano_ton ×1e9 · 9 dp 1884¢ × rate usdt · :micro_usdt ×1e6 · 6 dp 1884¢ × rate rub · :kopeck ×100 · 2 dp 1884¢ × rate
stars → a Star is its own minor unit (factor 1, 0 decimals — XTR has no sub-unit). price_minor(pkg, "stars", rates) returns stars_price verbatim: 1_449 Stars — the canonical face. No rate touches the base rail.

Codemojex.Rails holds the closed rail set ~w(stars ton usdt rub) as frozen module data — the minor unit, the factor, the decimals live in code, never in a mutable column. Rails.self_check!/0 is the boot vector: Codemojex.Application runs it at start-up, and it raises unless every rail’s factor == 10 ** decimals — the EchoData.BrandedId.self_check! pattern applied to money scaling.

§1

The catalog and the order

key_shop.ex · create_order/3

A Package is a versionable template — keys, stars_price (whole Stars, the canonical face), discount_pct, three nullable per-rail minor-unit overrides, enabled, sort. Editing it touches later orders only, because create_order/3 pins the money on the ORD at creation: the key count, the gross price_minor, and — for a non-Stars rail — the rate snapshot (rate_minor / rate_pair / rate_source / rate_quoted_at), read once from the key_shop_rates config. The order lands in created, and no keys are minted. The pricing itself is a pure function — no Repo, no clock, no HTTP:

echo/…/codemojex/key_shop.ex · price_minor/3 — the pure pricing source
def price_minor(package, "stars", _rates) do
  case Map.get(package, :stars_price) do
    p when is_integer(p) and p > 0 -> {:ok, p}                # the canonical face, verbatim
    _ -> {:error, :no_price}
  end
end

def price_minor(package, rail, rates) when rail in ~w(ton usdt rub) do
  case override(package, rail) do
    p when is_integer(p) and p > 0 ->
      {:ok, p}                                             # a pinned per-rail override wins

    _ ->
      with rate when is_integer(rate) and rate > 0 <- Map.get(rates, rail, :no_rate) do
        # minor = usd_face_cents × (rail minor units per USD cent), integer-exact, floored.
        {:ok, usd_face_cents(package, rates) * rate}
      else
        _ -> {:error, :no_rate}
      end
  end
end

def price_minor(_package, _rail, _rates), do: {:error, :bad_rail}

The client supplies only {package_id, rail} — the buy_keys route derives the key count and the price server-side, so a tampered client cannot name its own price (the keyshop story: create_order mints no keys; the client mint path is gone). The order statuses are the closed set created → paid → failed/refunded, checked at the changeset and again by the database. The seeded launch catalog is seven bundles, enabled and sorted — editable without a deploy.

§2

The Stars flow, end to end

telegram_controller.ex

The purchase begins at the facade. Codemojex.create_order/3 delegates to the KeyShop, and the buy_keys route returns the order plus a Stars XTR invoice link from Codemojex.Telegram.create_invoice_link/1 — with the ORD id as the invoice payload, so every later callback resolves back to the order it pays.

Telegram then sends pre_checkout_query. The webhook answers through KeyShop.valid_pre_checkout?/2, the tamper guard: true only when the order is still created and the presented amount equals the pinned price_minor. Fail-closed — a mismatch, an already-paid order, or an absent order is answered false.

successful_payment is the order-coupled confirmation. The controller hands it to settle_payment/1 — the OTX-gated fulfilment below. One rail, three callbacks, no polling: create_order → create_invoice_link → pre_checkout → successful_payment.

The full provider receipt is preserved verbatim on the OTX row’s raw_payload (jsonb) — the reconciliation read order_reconciliation/1 returns the pinned order, its receipts, and the revenue row it booked, keyed by the ORD ref.

§3

Exactly-once settlement

key_shop.ex · settle_payment/1

settle_payment/1 is one Repo.transaction under a FOR UPDATE order lock. It inserts the OTX receipt with on_conflict: :nothing on the (rail, external_id) partial unique index, then re-counts — the truth is the ledger, not the returned struct. Only if the count rose do the three writes fire. Select a delivery to run the settle over the fixed payment.

One payment, delivered twice — select a delivery
successful_payment telegram_controller.ex lock ORD FOR UPDATE insert OTX (rail, external_id) wrote? :wrote TXN +100 keys credit_purchase · ref=ORD RVL +1449 stars house_post · the gross ORD → paid mark_paid! 3 rows :fulfilled :suppressed OTX suppressed on_conflict: :nothing mutate nothing no mint · no booking 0 rows :already_fulfilled
first delivery → the OTX insert wrote — the (rail, external_id) count rose — so the gated writes fire: credit_purchase mints +100 keys (one TXN, ref = the ORD), house_post books +1_449 stars gross (one RVL row), mark_paid! flips the order. 3 rows; {:ok, :fulfilled}.
echo/…/codemojex/key_shop.ex · settle_payment/1 — the atomic fulfilment source
def settle_payment(%{order_id: order_id, rail: rail, external_id: external_id, amount_minor: amount_minor} = params) do
  payload = Map.get(params, :payload)

  Repo.transaction(fn ->
    case lock_order(order_id) do
      nil ->
        Repo.rollback(:no_order)

      order ->
        case insert_otx(order_id, rail, external_id, amount_minor, payload) do
          :suppressed ->
            # the (rail, external_id) dedup fired — a replay, mint NOTHING / book NOTHING
            :already_fulfilled

          :wrote ->
            # 1. MINT the keys (a players credit — always non-negative, A-7) keyed on the ORD.
            {:ok, _} = Wallet.credit_purchase(order.player, order.keys, order_id)
            # 2. BOOK the gross to the SAME revenue_ledger (cm-7 D-1/D-3) — account="platform",
            #    currency=the rail, reason="purchase", ref=the order id. ZERO DDL on revenue_ledger.
            Wallet.house_post(Wallet.house_account(), order.currency, amount_minor, "purchase", order_id)
            # 3. FLIP the order to paid (the only money-column-adjacent UPDATE; the price/rate stay pinned).
            mark_paid!(order)
            :fulfilled
        end
    end
  end)
end

defp lock_order(id), do: Repo.one(from o in Order, where: o.id == ^id, lock: "FOR UPDATE")

The index is the sole race-safe exactly-once authority — not a status == "paid" read-then-act, which carries a TOCTOU window the index closes. The insert’s conflict target names the migration’s predicate byte-for-byte: {:unsafe_fragment, "(rail, external_id) WHERE external_id IS NOT NULL"}. The keyshop story pins the behaviour: the 100-key bundle at gross 1449 Stars books one +1449 stars revenue row; a replayed successful_payment mints once and books once; the receipt, the key credit, and the revenue booking are all-or-nothing.

§4

The forward rails

schemas/webhook.ex · WHK

The pricing and the order flow are rail-complete today: the Order changeset admits all four rails, price_minor/3 prices each, and the settlement core is rail-generic — it takes {order_id, rail, external_id, amount_minor} and cares only that the pair is fresh. What only Stars has is a live confirmation surface: Telegram’s successful_payment.

Forward. The TON, USDT, and RUB payment verifiers are forward — schema-shaped and priced, with no live confirmation surface. So is the webhooks table: WHK is the designed-forward shape for the first push rail (on-chain TON), where an event arrives decoupled from an order and is deduped by (rail, event_id) at ingress — no migration creates that table this rung, and the schema has no live call site. For the Stars launch, WHK folds into the OTX (rail, external_id) index. Cash-out — money leaving the platform — is forward too (cm.8).

The gross a rail collects books into the same revenue_ledger read in C6.1; where the money goes out — cash-out and the treasury — is C6.3.

§5

References

sources · related

Sources

Related