@suluk/credits - v0.1.0
    Preparing search index...

    Function grantOnce

    @suluk/credits — a metered credit ledger (C046, extracted verbatim). The package OWNS the schema (credit_transaction

    • the credit_amount/credit_key sidecars); the app injects a Drizzle handle (D1 in prod, bun:sqlite in tests). The money-correctness core: the ATOMIC debitIfCovers (a conditional INSERT that can't drive the ledger negative under concurrency) + the idempotent debitOnceIfCovers (the partial-refund double-spend guard) + per-key spend + the activity-log query. App-specific payment-alert kinds + the user-table count stay in the app.
    • Idempotent money-IN grant — credit amount exactly once, keyed on the ledger row id idemKey (a STABLE per-payment anchor: pi:<id> / inv:<id> / cs:<id>), so a webhook redelivery or dashboard "Resend" can NEVER double-credit. The money-IN twin of debitOnceIfCovers (which guards money-OUT). legacyKey, when given, is an ADDITIONAL anchor honoured: if a row already exists under it the money is already credited and we skip — so MOVING the idempotency key across a deploy (e.g. event-id → session-id) can't re-grant an in-flight payment. This is the LEDGER-INTEGRITY chokepoint for every grant: it rejects a non-finite / non-integer / non-positive delta, so an upstream Number(metadata.credits) can never let "Infinity" (which would poison every later balance read) or "500.9" (which would break balance == SUM(int delta)) reach the ledger. Returns true ONLY on a FRESH grant; on a fresh grant the cash amountCents (when given) is annotated for the $ detail. Use this for Stripe webhook crediting (top-up / subscription).

      Parameters

      • db: CreditsDB
      • userId: string
      • amount: number
      • idemKey: string
      • reason: string = "grant"
      • OptionalamountCents: number | null
      • OptionallegacyKey: string

      Returns Promise<boolean>