feat(stripe): auto-bootstrap default product on first config save (BUNYIP-189) #212

Merged
YousifShkara merged 1 commit from feat/BUNYIP-189-auto-bootstrap-product into main 2026-06-24 10:07:25 +02:00
Owner

PMS-A-5 gotcha 3 catalogued the silent-400 trap: a fresh Stripe account
has no products with the app=<STRIPE_APP_TAG> metadata tag, so the
checkout handler's list_prices returns empty, the api 400s with
price_id: No active price configured, and (until BUNYIP-187) the
Subscribe button silently fails. The documented workaround is an out-of-
band stripe products create -d "metadata[app]=bunyip" + stripe prices create ... sequence, which is a poor onboarding experience on dev and
on every new live deployment.

What landed:

  • New StripeService::bootstrap_default_product_if_missing(). Idempotent
    by design: lists app-tagged active prices first and returns
    Ok(None) when any exist. Otherwise creates a "{APP_NAME} Membership"
    product (uses the existing create_product which auto-injects the
    app= metadata tag) + a recurring month-interval $3 USD price.
    Defaults live in env vars so an operator can override without
    recompiling:

    • BUNYIP_DEFAULT_PRICE_AMOUNT_CENTS (default 300)
    • BUNYIP_DEFAULT_PRICE_CURRENCY (default usd)
    • BUNYIP_DEFAULT_PRICE_INTERVAL (default month)
    • APP_NAME (default Bunyip for the name)
      Returns Ok(Some((product, price))) on a fresh bootstrap so the
      caller can audit-log what was created.
  • update_stripe_config (PUT /v1/admin/stripe) calls the bootstrap
    after the hot-reload that picks up the new secret key. The audit-log
    metadata now carries a bootstrap: { product_id, price_id } block
    on the save that produced the auto-create, so an operator chasing
    "where did this product come from" can trace it to the exact config
    save. Bootstrap failure does NOT fail the config save (keys are
    valid; admin can still create the product manually); the failure is
    logged at warn.

Round trip on a fresh test-mode key: PUT /v1/admin/stripe with a real
sk_test_... -> bootstrap fires once, product + price land in Stripe
tagged with app=bunyip -> Subscribe button now opens checkout.

#BUNYIP-189

PMS-A-5 gotcha 3 catalogued the silent-400 trap: a fresh Stripe account has no products with the `app=<STRIPE_APP_TAG>` metadata tag, so the checkout handler's `list_prices` returns empty, the api 400s with `price_id: No active price configured`, and (until BUNYIP-187) the Subscribe button silently fails. The documented workaround is an out-of- band `stripe products create -d "metadata[app]=bunyip"` + `stripe prices create ...` sequence, which is a poor onboarding experience on dev and on every new live deployment. What landed: * New `StripeService::bootstrap_default_product_if_missing()`. Idempotent by design: lists app-tagged active prices first and returns `Ok(None)` when any exist. Otherwise creates a `"{APP_NAME} Membership"` product (uses the existing `create_product` which auto-injects the `app=` metadata tag) + a recurring `month`-interval `$3 USD` price. Defaults live in env vars so an operator can override without recompiling: - `BUNYIP_DEFAULT_PRICE_AMOUNT_CENTS` (default `300`) - `BUNYIP_DEFAULT_PRICE_CURRENCY` (default `usd`) - `BUNYIP_DEFAULT_PRICE_INTERVAL` (default `month`) - `APP_NAME` (default `Bunyip` for the name) Returns `Ok(Some((product, price)))` on a fresh bootstrap so the caller can audit-log what was created. * `update_stripe_config` (PUT /v1/admin/stripe) calls the bootstrap after the hot-reload that picks up the new secret key. The audit-log metadata now carries a `bootstrap: { product_id, price_id }` block on the save that produced the auto-create, so an operator chasing "where did this product come from" can trace it to the exact config save. Bootstrap failure does NOT fail the config save (keys are valid; admin can still create the product manually); the failure is logged at warn. Round trip on a fresh test-mode key: PUT /v1/admin/stripe with a real `sk_test_...` -> bootstrap fires once, product + price land in Stripe tagged with `app=bunyip` -> Subscribe button now opens checkout. #BUNYIP-189
feat(stripe): auto-bootstrap default product on first config save (BUNYIP-189)
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 2m41s
E2E / Playwright against deployment (pull_request) Successful in 47s
Create release / Create release from merged PR (pull_request) Has been skipped
fa1cb5a533
PMS-A-5 gotcha 3 catalogued the silent-400 trap: a fresh Stripe account
has no products with the `app=<STRIPE_APP_TAG>` metadata tag, so the
checkout handler's `list_prices` returns empty, the api 400s with
`price_id: No active price configured`, and (until BUNYIP-187) the
Subscribe button silently fails. The documented workaround is an out-of-
band `stripe products create -d "metadata[app]=bunyip"` + `stripe prices
create ...` sequence, which is a poor onboarding experience on dev and
on every new live deployment.

What landed:

* New `StripeService::bootstrap_default_product_if_missing()`. Idempotent
  by design: lists app-tagged active prices first and returns
  `Ok(None)` when any exist. Otherwise creates a `"{APP_NAME} Membership"`
  product (uses the existing `create_product` which auto-injects the
  `app=` metadata tag) + a recurring `month`-interval `$3 USD` price.
  Defaults live in env vars so an operator can override without
  recompiling:
    - `BUNYIP_DEFAULT_PRICE_AMOUNT_CENTS` (default `300`)
    - `BUNYIP_DEFAULT_PRICE_CURRENCY`     (default `usd`)
    - `BUNYIP_DEFAULT_PRICE_INTERVAL`     (default `month`)
    - `APP_NAME`                          (default `Bunyip` for the name)
  Returns `Ok(Some((product, price)))` on a fresh bootstrap so the
  caller can audit-log what was created.

* `update_stripe_config` (PUT /v1/admin/stripe) calls the bootstrap
  after the hot-reload that picks up the new secret key. The audit-log
  metadata now carries a `bootstrap: { product_id, price_id }` block
  on the save that produced the auto-create, so an operator chasing
  "where did this product come from" can trace it to the exact config
  save. Bootstrap failure does NOT fail the config save (keys are
  valid; admin can still create the product manually); the failure is
  logged at warn.

Round trip on a fresh test-mode key: PUT /v1/admin/stripe with a real
`sk_test_...` -> bootstrap fires once, product + price land in Stripe
tagged with `app=bunyip` -> Subscribe button now opens checkout.

#BUNYIP-189
YousifShkara deleted branch feat/BUNYIP-189-auto-bootstrap-product 2026-06-24 10:07:26 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/bunyip!212
No description provided.