Class: PricingPlans::Plan
- Inherits:
-
Object
- Object
- PricingPlans::Plan
- Defined in:
- lib/pricing_plans/plan.rb
Instance Attribute Summary collapse
-
#features ⇒ Object
readonly
Returns the value of attribute features.
-
#key ⇒ Object
readonly
Returns the value of attribute key.
Instance Method Summary collapse
- #allow(*feature_keys) ⇒ Object
-
#allows(*feature_keys) ⇒ Object
Feature methods.
- #allows_feature?(feature_key) ⇒ Boolean
- #bullets(*values) ⇒ Object
- #credits_included(value = :__get__) ⇒ Object
- #cta_text(value = nil) ⇒ Object
-
#cta_url(value = :__no_arg__, plan_owner: nil) ⇒ Object
Unified ergonomic API: - Setter/getter: cta_url, cta_url("/checkout") - Resolver: cta_url(plan_owner: org).
- #currency_symbol ⇒ Object
-
#current_for?(current_plan) ⇒ Boolean
Plan comparison helpers for CTA ergonomics.
-
#default!(value = true) ⇒ Object
Plan selection sugar.
- #default? ⇒ Boolean
- #description(value = nil) ⇒ Object
- #disallow(*feature_keys) ⇒ Object
- #disallows(*feature_keys) ⇒ Object
- #downgrade_blocked_reason(from: nil, plan_owner: nil) ⇒ Object
- #downgrade_from?(current_plan) ⇒ Boolean
-
#free? ⇒ Boolean
Ergonomic predicate for UI/logic (free means explicit 0 price or explicit "Free" label).
- #has_interval_prices? ⇒ Boolean
- #has_numeric_price? ⇒ Boolean
- #hidden!(value = true) ⇒ Object
- #hidden? ⇒ Boolean
- #highlighted!(value = true) ⇒ Object
- #highlighted? ⇒ Boolean
-
#includes_credits(amount) ⇒ Object
Credits display methods (cosmetic, for pricing UI) Single-currency credits.
-
#initialize(key) ⇒ Plan
constructor
A new instance of Plan.
- #limit(key, **options) ⇒ Object
- #limit_for(key) ⇒ Object
- #limits(key = nil, **options) ⇒ Object
- #meta(values = nil) ⇒ Object (also: #metadata)
-
#monthly_price_cents ⇒ Object
Stripe convenience accessors (nil when interval not present).
- #monthly_price_components ⇒ Object
- #monthly_price_id ⇒ Object
- #name(value = nil) ⇒ Object
-
#popular? ⇒ Boolean
Syntactic sugar for popular/highlighted.
- #price(value = nil) ⇒ Object
-
#price_cents ⇒ Object
Rails-y ergonomics for UI: expose integer cents as optional helper.
-
#price_components(interval: :month) ⇒ Object
Compute semantic price parts for the given interval (:month or :year).
-
#price_label ⇒ Object
Human label to display price in UIs.
- #price_label_for(interval) ⇒ Object
- #price_string(value = nil) ⇒ Object
-
#purchasable? ⇒ Boolean
Convenience booleans used by views/hosts (keep single definition above).
- #set_bullets(*values) ⇒ Object
-
#set_cta_text(value) ⇒ Object
CTA helpers for pricing UI.
- #set_cta_url(value) ⇒ Object
- #set_description(value) ⇒ Object
-
#set_limit(key, **options) ⇒ Object
Limit methods.
- #set_meta(values) ⇒ Object (also: #set_metadata)
-
#set_name(value) ⇒ Object
DSL methods for plan configuration.
- #set_price(value) ⇒ Object
- #set_price_string(value) ⇒ Object
- #set_stripe_price(value) ⇒ Object
- #stripe_price(value = nil) ⇒ Object
-
#to_view_model ⇒ Object
Pure-data view model for JS/Hotwire.
- #unlimited(*keys) ⇒ Object
- #upgrade_from?(current_plan) ⇒ Boolean
- #validate! ⇒ Object
- #yearly_price_cents ⇒ Object
- #yearly_price_components ⇒ Object
- #yearly_price_id ⇒ Object
Constructor Details
#initialize(key) ⇒ Plan
Returns a new instance of Plan.
11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
# File 'lib/pricing_plans/plan.rb', line 11 def initialize(key) @key = key @name = nil @description = nil @bullets = [] @price = nil @price_string = nil @stripe_price = nil @features = Set.new @limits = {} @credits_included = nil @meta = {} @cta_text = nil @cta_url = nil @default = false @highlighted = false @hidden = false end |
Instance Attribute Details
#features ⇒ Object (readonly)
Returns the value of attribute features.
9 10 11 |
# File 'lib/pricing_plans/plan.rb', line 9 def features @features end |
#key ⇒ Object (readonly)
Returns the value of attribute key.
9 10 11 |
# File 'lib/pricing_plans/plan.rb', line 9 def key @key end |
Instance Method Details
#allow(*feature_keys) ⇒ Object
188 189 190 |
# File 'lib/pricing_plans/plan.rb', line 188 def allow(*feature_keys) allows(*feature_keys) end |
#allows(*feature_keys) ⇒ Object
Feature methods
182 183 184 185 186 |
# File 'lib/pricing_plans/plan.rb', line 182 def allows(*feature_keys) feature_keys.flatten.each do |key| @features.add(key.to_sym) end end |
#allows_feature?(feature_key) ⇒ Boolean
202 203 204 |
# File 'lib/pricing_plans/plan.rb', line 202 def allows_feature?(feature_key) @features.include?(feature_key.to_sym) end |
#bullets(*values) ⇒ Object
59 60 61 62 63 64 65 |
# File 'lib/pricing_plans/plan.rb', line 59 def bullets(*values) if values.empty? @bullets else set_bullets(*values) end end |
#credits_included(value = :__get__) ⇒ Object
250 251 252 253 254 255 256 |
# File 'lib/pricing_plans/plan.rb', line 250 def credits_included(value = :__get__) if value == :__get__ @credits_included else @credits_included = value.to_i end end |
#cta_text(value = nil) ⇒ Object
150 151 152 153 154 155 156 |
# File 'lib/pricing_plans/plan.rb', line 150 def cta_text(value = nil) if value.nil? @cta_text || PricingPlans.configuration.default_cta_text || default_cta_text_derived else set_cta_text(value) end end |
#cta_url(value = :__no_arg__, plan_owner: nil) ⇒ Object
Unified ergonomic API:
- Setter/getter: cta_url, cta_url("/checkout")
- Resolver: cta_url(plan_owner: org)
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 |
# File 'lib/pricing_plans/plan.rb', line 165 def cta_url(value = :__no_arg__, plan_owner: nil) unless value == :__no_arg__ set_cta_url(value) return @cta_url end return @cta_url if @cta_url default = PricingPlans.configuration.default_cta_url return default if default # New default: if host app defines subscribe_path, prefer that if defined?(Rails) && Rails.respond_to?(:application) && Rails.application && Rails.application.routes.url_helpers.respond_to?(:subscribe_path) return Rails.application.routes.url_helpers.subscribe_path(plan: key, interval: :month) end nil end |
#currency_symbol ⇒ Object
447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 |
# File 'lib/pricing_plans/plan.rb', line 447 def currency_symbol # A locally declared numeric price is rendered in the configured currency # (see #price_components), so don't ask Stripe when we have one. if stripe_price && price.nil? # Try to derive from Stripe API/cache; fall back to default begin pr = fetch_stripe_price_record(preferred_price_id(:month) || preferred_price_id(:year)) if pr return currency_symbol_from(pr) end rescue StandardError # Stripe unreachable, rate-limited or unconfigured: never take down a pricing page end end PricingPlans.configuration.default_currency_symbol end |
#current_for?(current_plan) ⇒ Boolean
Plan comparison helpers for CTA ergonomics
465 466 467 468 |
# File 'lib/pricing_plans/plan.rb', line 465 def current_for?(current_plan) return false unless current_plan current_plan.key.to_sym == key.to_sym end |
#default!(value = true) ⇒ Object
Plan selection sugar
259 260 261 |
# File 'lib/pricing_plans/plan.rb', line 259 def default!(value = true) @default = !!value end |
#default? ⇒ Boolean
263 264 265 |
# File 'lib/pricing_plans/plan.rb', line 263 def default? !!@default end |
#description(value = nil) ⇒ Object
47 48 49 50 51 52 53 |
# File 'lib/pricing_plans/plan.rb', line 47 def description(value = nil) if value.nil? @description else set_description(value) end end |
#disallow(*feature_keys) ⇒ Object
198 199 200 |
# File 'lib/pricing_plans/plan.rb', line 198 def disallow(*feature_keys) disallows(*feature_keys) end |
#disallows(*feature_keys) ⇒ Object
192 193 194 195 196 |
# File 'lib/pricing_plans/plan.rb', line 192 def disallows(*feature_keys) feature_keys.flatten.each do |key| @features.delete(key.to_sym) end end |
#downgrade_blocked_reason(from: nil, plan_owner: nil) ⇒ Object
480 481 482 483 484 |
# File 'lib/pricing_plans/plan.rb', line 480 def downgrade_blocked_reason(from: nil, plan_owner: nil) return nil unless from allowed, reason = PricingPlans.configuration.downgrade_policy.call(from: from, to: self, plan_owner: plan_owner) allowed ? nil : (reason || "Downgrade not allowed") end |
#downgrade_from?(current_plan) ⇒ Boolean
475 476 477 478 |
# File 'lib/pricing_plans/plan.rb', line 475 def downgrade_from?(current_plan) return false unless current_plan comparable_price_cents(self) < comparable_price_cents(current_plan) end |
#free? ⇒ Boolean
Ergonomic predicate for UI/logic (free means explicit 0 price or explicit "Free" label)
92 93 94 95 96 97 |
# File 'lib/pricing_plans/plan.rb', line 92 def free? return false if @stripe_price return true if @price.respond_to?(:to_i) && @price.to_i.zero? return true if @price_string && @price_string.to_s.strip.casecmp("Free").zero? false end |
#has_interval_prices? ⇒ Boolean
413 414 415 416 417 |
# File 'lib/pricing_plans/plan.rb', line 413 def has_interval_prices? sp = stripe_price return true if sp.is_a?(Hash) && (sp[:month] || sp[:year]) return !price.nil? || !price_string.nil? end |
#has_numeric_price? ⇒ Boolean
419 420 421 |
# File 'lib/pricing_plans/plan.rb', line 419 def has_numeric_price? !!price || !!stripe_price end |
#hidden!(value = true) ⇒ Object
282 283 284 |
# File 'lib/pricing_plans/plan.rb', line 282 def hidden!(value = true) @hidden = !!value end |
#hidden? ⇒ Boolean
286 287 288 |
# File 'lib/pricing_plans/plan.rb', line 286 def hidden? !!@hidden end |
#highlighted!(value = true) ⇒ Object
267 268 269 |
# File 'lib/pricing_plans/plan.rb', line 267 def highlighted!(value = true) @highlighted = !!value end |
#highlighted? ⇒ Boolean
271 272 273 274 275 276 277 278 279 280 |
# File 'lib/pricing_plans/plan.rb', line 271 def highlighted? return true if @highlighted # Treat configuration.highlighted_plan as highlighted without consulting Registry to avoid recursion begin cfg = PricingPlans.configuration return true if cfg && cfg.highlighted_plan && cfg.highlighted_plan.to_sym == @key rescue StandardError end false end |
#includes_credits(amount) ⇒ Object
Credits display methods (cosmetic, for pricing UI) Single-currency credits. We do not tie credits to operations here.
246 247 248 |
# File 'lib/pricing_plans/plan.rb', line 246 def includes_credits(amount) @credits_included = amount.to_i end |
#limit(key, **options) ⇒ Object
230 231 232 |
# File 'lib/pricing_plans/plan.rb', line 230 def limit(key, **) set_limit(key, **) end |
#limit_for(key) ⇒ Object
240 241 242 |
# File 'lib/pricing_plans/plan.rb', line 240 def limit_for(key) @limits[key.to_sym] end |
#limits(key = nil, **options) ⇒ Object
222 223 224 225 226 227 228 |
# File 'lib/pricing_plans/plan.rb', line 222 def limits(key=nil, **) if key.nil? @limits else set_limit(key, **) end end |
#meta(values = nil) ⇒ Object Also known as: metadata
134 135 136 137 138 139 140 |
# File 'lib/pricing_plans/plan.rb', line 134 def (values = nil) if values.nil? @meta else (values) end end |
#monthly_price_cents ⇒ Object
Stripe convenience accessors (nil when interval not present)
429 430 431 432 |
# File 'lib/pricing_plans/plan.rb', line 429 def monthly_price_cents pc = monthly_price_components pc.present? ? pc.amount_cents : nil end |
#monthly_price_components ⇒ Object
405 406 407 |
# File 'lib/pricing_plans/plan.rb', line 405 def monthly_price_components price_components(interval: :month) end |
#monthly_price_id ⇒ Object
439 440 441 |
# File 'lib/pricing_plans/plan.rb', line 439 def monthly_price_id stripe_price_id_for(:month) end |
#name(value = nil) ⇒ Object
35 36 37 38 39 40 41 |
# File 'lib/pricing_plans/plan.rb', line 35 def name(value = nil) if value.nil? @name || @key.to_s.titleize else set_name(value) end end |
#popular? ⇒ Boolean
Syntactic sugar for popular/highlighted
291 292 293 |
# File 'lib/pricing_plans/plan.rb', line 291 def popular? highlighted? end |
#price(value = nil) ⇒ Object
71 72 73 74 75 76 77 |
# File 'lib/pricing_plans/plan.rb', line 71 def price(value = nil) if value.nil? @price else set_price(value) end end |
#price_cents ⇒ Object
Rails-y ergonomics for UI: expose integer cents as optional helper
80 81 82 83 84 85 86 87 88 89 |
# File 'lib/pricing_plans/plan.rb', line 80 def price_cents return nil unless @price ( if @price.respond_to?(:to_f) (@price.to_f * 100).round else nil end ) end |
#price_components(interval: :month) ⇒ Object
Compute semantic price parts for the given interval (:month or :year). Falls back to price_string when no numeric price exists.
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 |
# File 'lib/pricing_plans/plan.rb', line 343 def price_components(interval: :month) # 1) Allow app override if (resolver = PricingPlans.configuration.price_components_resolver) begin resolved = resolver.call(self, interval) return resolved if resolved rescue StandardError end end # 2) String-only prices if price_string return PricingPlans::PriceComponents.new( present?: false, currency: nil, amount: nil, amount_cents: nil, interval: interval, label: price_string, monthly_equivalent_cents: nil ) end # 3) Explicit numeric price (single interval, assume monthly semantics) if price cents = price_cents cur = PricingPlans.configuration.default_currency_symbol label = if interval == :month "#{cur}#{price}/mo" else # Treat yearly as 12x when only a single numeric price is declared "#{cur}#{(price.to_f * 12).round}/yr" end return PricingPlans::PriceComponents.new( present?: true, currency: cur, amount: (interval == :month ? price.to_i : (price.to_f * 12).round).to_s, amount_cents: (interval == :month ? cents : (cents.to_i * 12)), interval: interval, label: label, monthly_equivalent_cents: cents ) end # 4) Stripe price(s) if stripe_price comp = stripe_price_components(interval) return comp if comp end # 5) No price info at all → Contact PricingPlans::PriceComponents.new( present?: false, currency: nil, amount: nil, amount_cents: nil, interval: interval, label: "Contact", monthly_equivalent_cents: nil ) end |
#price_label ⇒ Object
Human label to display price in UIs. Prefers explicit string, then numeric, else contact.
303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 |
# File 'lib/pricing_plans/plan.rb', line 303 def price_label # Auto-fetch from processor (Stripe) if enabled and plan has stripe_price. # A locally declared numeric price wins: it is the source of truth for # display, and honoring it keeps rendering off the network entirely. cfg = PricingPlans.configuration if cfg&.auto_price_labels_from_processor && stripe_price && price.nil? begin if defined?(::Stripe) price_id = stripe_price.is_a?(Hash) ? (stripe_price[:id] || stripe_price[:month] || stripe_price[:year]) : stripe_price if price_id pr = ::Stripe::Price.retrieve(price_id) amount = pr.unit_amount.to_f / 100.0 interval = pr.recurring&.interval suffix = interval ? "/#{interval[0,3]}" : "" return "$#{amount}#{suffix}" end end rescue StandardError # fallthrough to local derivation end end # Allow host app override via resolver if cfg&.price_label_resolver begin built = cfg.price_label_resolver.call(self) return built if built rescue StandardError end end return "Free" if price && price.to_i.zero? return price_string if price_string return "$#{price}/mo" if price return "Contact" if stripe_price || price.nil? nil end |
#price_label_for(interval) ⇒ Object
423 424 425 426 |
# File 'lib/pricing_plans/plan.rb', line 423 def price_label_for(interval) pc = price_components(interval: interval) pc.label end |
#price_string(value = nil) ⇒ Object
103 104 105 106 107 108 109 |
# File 'lib/pricing_plans/plan.rb', line 103 def price_string(value = nil) if value.nil? @price_string else set_price_string(value) end end |
#purchasable? ⇒ Boolean
Convenience booleans used by views/hosts (keep single definition above)
298 299 300 |
# File 'lib/pricing_plans/plan.rb', line 298 def purchasable? !!@stripe_price || (!free? && !!@price) end |
#set_bullets(*values) ⇒ Object
55 56 57 |
# File 'lib/pricing_plans/plan.rb', line 55 def set_bullets(*values) @bullets = values.flatten.map(&:to_s) end |
#set_cta_text(value) ⇒ Object
CTA helpers for pricing UI
146 147 148 |
# File 'lib/pricing_plans/plan.rb', line 146 def set_cta_text(value) @cta_text = value&.to_s end |
#set_cta_url(value) ⇒ Object
158 159 160 |
# File 'lib/pricing_plans/plan.rb', line 158 def set_cta_url(value) @cta_url = value&.to_s end |
#set_description(value) ⇒ Object
43 44 45 |
# File 'lib/pricing_plans/plan.rb', line 43 def set_description(value) @description = value.to_s end |
#set_limit(key, **options) ⇒ Object
Limit methods
207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
# File 'lib/pricing_plans/plan.rb', line 207 def set_limit(key, **) limit_key = key.to_sym @limits[limit_key] = { key: limit_key, to: [:to], per: [:per], after_limit: .fetch(:after_limit, :block_usage), grace: .fetch(:grace, 7.days), warn_at: .fetch(:warn_at, [0.6, 0.8, 0.95]), count_scope: [:count_scope] } (@limits[limit_key]) end |
#set_meta(values) ⇒ Object Also known as: set_metadata
130 131 132 |
# File 'lib/pricing_plans/plan.rb', line 130 def (values) @meta.merge!(values) end |
#set_name(value) ⇒ Object
DSL methods for plan configuration
31 32 33 |
# File 'lib/pricing_plans/plan.rb', line 31 def set_name(value) @name = value.to_s end |
#set_price(value) ⇒ Object
67 68 69 |
# File 'lib/pricing_plans/plan.rb', line 67 def set_price(value) @price = value end |
#set_price_string(value) ⇒ Object
99 100 101 |
# File 'lib/pricing_plans/plan.rb', line 99 def set_price_string(value) @price_string = value.to_s end |
#set_stripe_price(value) ⇒ Object
111 112 113 114 115 116 117 118 119 120 |
# File 'lib/pricing_plans/plan.rb', line 111 def set_stripe_price(value) case value when String @stripe_price = { id: value } when Hash @stripe_price = value else raise ConfigurationError, "stripe_price must be a string or hash" end end |
#stripe_price(value = nil) ⇒ Object
122 123 124 125 126 127 128 |
# File 'lib/pricing_plans/plan.rb', line 122 def stripe_price(value = nil) if value.nil? @stripe_price else set_stripe_price(value) end end |
#to_view_model ⇒ Object
Pure-data view model for JS/Hotwire
487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 |
# File 'lib/pricing_plans/plan.rb', line 487 def to_view_model { id: key.to_s, key: key.to_s, name: name, description: description, features: bullets, # alias in this gem metadata: .dup, highlighted: highlighted?, default: default?, free: free?, currency: currency_symbol, monthly_price_cents: monthly_price_cents, yearly_price_cents: yearly_price_cents, monthly_price_id: monthly_price_id, yearly_price_id: yearly_price_id, price_label: price_label, price_string: price_string, limits: limits.transform_values { |v| v.dup } } end |
#unlimited(*keys) ⇒ Object
234 235 236 237 238 |
# File 'lib/pricing_plans/plan.rb', line 234 def unlimited(*keys) keys.flatten.each do |key| set_limit(key.to_sym, to: :unlimited) end end |
#upgrade_from?(current_plan) ⇒ Boolean
470 471 472 473 |
# File 'lib/pricing_plans/plan.rb', line 470 def upgrade_from?(current_plan) return false unless current_plan comparable_price_cents(self) > comparable_price_cents(current_plan) end |
#validate! ⇒ Object
509 510 511 512 |
# File 'lib/pricing_plans/plan.rb', line 509 def validate! validate_limits! validate_pricing! end |
#yearly_price_cents ⇒ Object
434 435 436 437 |
# File 'lib/pricing_plans/plan.rb', line 434 def yearly_price_cents pc = yearly_price_components pc.present? ? pc.amount_cents : nil end |
#yearly_price_components ⇒ Object
409 410 411 |
# File 'lib/pricing_plans/plan.rb', line 409 def yearly_price_components price_components(interval: :year) end |
#yearly_price_id ⇒ Object
443 444 445 |
# File 'lib/pricing_plans/plan.rb', line 443 def yearly_price_id stripe_price_id_for(:year) end |