Prepaid bundles: bundle Stripe Price + coupon on the pricing policy

Store the one-time bundle Price id + coupon id per pricing policy (same home as
the metered price ids) instead of an edge-fn env var / lookup key — ops point a
policy at its Stripe objects with a SQL UPDATE, no redeploy.

- V42: pricing_policy.bundle_stripe_price_id + bundle_coupon_id (nullable;
  additive/idempotent). NULL = bundles not offered for that policy.
- PricingPolicy entity gains the two fields.

The create-payg-bundle-checkout edge fn (SaaS repo) now reads them via the
payg_get_bundle_checkout_context RPC.
This commit is contained in:
Connor Yoh
2026-07-15 14:02:39 +01:00
parent edf688ef09
commit 75006b655a
2 changed files with 35 additions and 0 deletions
@@ -114,6 +114,19 @@ public class PricingPolicy implements Serializable {
@Column(name = "stripe_price_id", nullable = false, length = 128)
private Set<String> stripePriceIds = new HashSet<>();
/**
* One-time Stripe Price id for prepaid-bundle checkout — same {@code unit_amount} as the
* metered price (Stripe {@code currency_options} cover all currencies on one Price). Read by
* the create-payg-bundle-checkout edge fn via {@code payg_get_bundle_checkout_context}. Null =
* bundles not offered for this policy. Money lives in Stripe; this is just the handle.
*/
@Column(name = "bundle_stripe_price_id", length = 128)
private String bundleStripePriceId;
/** Stripe coupon id applying the 12-for-10 prepaid discount. Null = bundles not offered. */
@Column(name = "bundle_coupon_id", length = 128)
private String bundleCouponId;
/**
* Exactly one row in the table has {@code is_default = true}; enforced by partial unique idx.
*/
@@ -0,0 +1,22 @@
-- Prepaid-bundle Stripe objects on the pricing policy — same home as the metered price ids
-- (pricing_policy_stripe_price). The bundle is bought at a one-time Price whose unit_amount matches
-- the meter, discounted by a coupon; both are per-policy config so ops can point a policy at its
-- Stripe objects without an env var or code change.
--
-- * bundle_stripe_price_id — the ONE-TIME Price (Stripe currency_options cover all currencies on a
-- single Price, so one id per policy, exactly like the metered price).
-- * bundle_coupon_id — the "12 months for the price of 10" coupon.
--
-- NULL on both = bundles not offered for that policy; the create-payg-bundle-checkout edge fn refuses
-- to mint a session. Additive + idempotent. Money still lives in Stripe — these are just the handles.
ALTER TABLE stirling_pdf.pricing_policy
ADD COLUMN IF NOT EXISTS bundle_stripe_price_id VARCHAR(128);
ALTER TABLE stirling_pdf.pricing_policy
ADD COLUMN IF NOT EXISTS bundle_coupon_id VARCHAR(128);
COMMENT ON COLUMN stirling_pdf.pricing_policy.bundle_stripe_price_id IS
'One-time Stripe Price id for prepaid-bundle checkout (same unit_amount as the metered price). '
'NULL = bundles not offered for this policy.';
COMMENT ON COLUMN stirling_pdf.pricing_policy.bundle_coupon_id IS
'Stripe coupon id applying the 12-for-10 prepaid discount. NULL = bundles not offered.';