From 75006b655a7c663bfe5f031e3fbd8b9ef3b44b03 Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Wed, 15 Jul 2026 14:02:39 +0100 Subject: [PATCH] Prepaid bundles: bundle Stripe Price + coupon on the pricing policy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../saas/payg/policy/PricingPolicy.java | 13 +++++++++++ .../saas/V42__payg_bundle_pricing_policy.sql | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 app/saas/src/main/resources/db/migration/saas/V42__payg_bundle_pricing_policy.sql diff --git a/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicy.java b/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicy.java index 9581177369..d28b0fd09d 100644 --- a/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicy.java +++ b/app/saas/src/main/java/stirling/software/saas/payg/policy/PricingPolicy.java @@ -114,6 +114,19 @@ public class PricingPolicy implements Serializable { @Column(name = "stripe_price_id", nullable = false, length = 128) private Set 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. */ diff --git a/app/saas/src/main/resources/db/migration/saas/V42__payg_bundle_pricing_policy.sql b/app/saas/src/main/resources/db/migration/saas/V42__payg_bundle_pricing_policy.sql new file mode 100644 index 0000000000..92369fdc33 --- /dev/null +++ b/app/saas/src/main/resources/db/migration/saas/V42__payg_bundle_pricing_policy.sql @@ -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.';