Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 29 additions & 29 deletions app/helpers/design_system_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,95 +6,95 @@ module DesignSystemHelper
BTN_ACCENT = "#{BTN_BASE} bg-gradient-accent text-white shadow-md hover:shadow-lg focus:ring-accent-700/50 hover:brightness-110"
BTN_GHOST = "#{BTN_BASE} bg-transparent text-primary-600 hover:bg-primary-50 focus:ring-primary-500/50"
BTN_DANGER = "#{BTN_BASE} bg-error text-white shadow-md hover:shadow-lg focus:ring-error/50 hover:brightness-110"

# Button Sizes
BTN_SM = "px-4 py-2 text-sm rounded-lg"
BTN_LG = "px-8 py-4 text-lg rounded-2xl"
BTN_XL = "px-10 py-5 text-xl rounded-2xl"

# Card Classes
CARD = "bg-white rounded-2xl shadow-md border border-neutral-100 overflow-hidden transition-all duration-300 hover:shadow-lg"
CARD_BODY = "p-6 md:p-8"
CARD_HEADER = "px-6 py-4 md:px-8 md:py-6 border-b border-neutral-100 bg-gradient-to-b from-neutral-50 to-transparent"

# Alert Classes
ALERT_BASE = "p-4 rounded-xl border backdrop-blur-sm animate-slide-up"
ALERT_SUCCESS = "#{ALERT_BASE} bg-success/10 border-success/20 text-success"
ALERT_ERROR = "#{ALERT_BASE} bg-error/10 border-error/20 text-error"
ALERT_WARNING = "#{ALERT_BASE} bg-warning/10 border-warning/20 text-warning"
ALERT_INFO = "#{ALERT_BASE} bg-info/10 border-info/20 text-info"

# Form Classes
FORM_INPUT = "w-full px-4 py-3 text-base bg-white border border-neutral-200 rounded-xl shadow-sm transition-all duration-200 focus:outline-none focus:ring-4 focus:ring-primary-500/20 focus:border-primary-500 placeholder:text-neutral-400"
FORM_LABEL = "block text-sm font-medium text-neutral-700 mb-2"
FORM_ERROR = "text-sm text-error mt-1"

# Badge Classes
BADGE_BASE = "inline-flex items-center px-3 py-1 text-sm font-medium rounded-full"
BADGE_PRIMARY = "#{BADGE_BASE} bg-primary-100 text-primary-700"
BADGE_SUCCESS = "#{BADGE_BASE} bg-success/10 text-success"
BADGE_WARNING = "#{BADGE_BASE} bg-warning/10 text-warning"

# Link Classes
LINK = "text-primary-600 underline decoration-primary-300 decoration-2 underline-offset-2 hover:decoration-primary-600 transition-colors duration-200"
LINK_SUBTLE = "text-neutral-600 no-underline hover:text-primary-600 hover:underline transition-all duration-200"

# Hero Classes
HERO = "relative py-20 md:py-32 overflow-hidden"
HERO_CONTENT = "relative z-10 max-w-4xl mx-auto text-center px-4"
HERO_TITLE = "text-5xl md:text-6xl font-bold text-primary-900 mb-6 animate-fade-in"
HERO_SUBTITLE = "text-xl md:text-2xl text-neutral-600 mb-8 animate-slide-up"

# Navigation Classes
NAV_LINK = "px-4 py-2 text-base font-medium text-neutral-600 rounded-lg transition-all duration-200 hover:bg-primary-50 hover:text-primary-700"
NAV_LINK_ACTIVE = "#{NAV_LINK} bg-primary-50 text-primary-700"

# Section Classes
SECTION = "py-16 md:py-24"
SECTION_TITLE = "text-3xl md:text-4xl font-bold text-primary-900 mb-4"
SECTION_SUBTITLE = "text-lg md:text-xl text-neutral-600"

# Container Classes
CONTAINER_NARROW = "max-w-4xl mx-auto px-4"
CONTAINER_WIDE = "max-w-7xl mx-auto px-4"

# Other Classes
DIVIDER = "h-px bg-gradient-to-r from-transparent via-neutral-200 to-transparent my-8"
SKELETON = "animate-pulse bg-neutral-200 rounded"
SPINNER = "inline-block w-5 h-5 border-2 border-primary-600 border-t-transparent rounded-full animate-spin"

def btn_primary(size = nil)
classes = [BTN_PRIMARY]
classes = [ BTN_PRIMARY ]
classes << btn_size_class(size) if size
classes.join(' ')
classes.join(" ")
end

def btn_secondary(size = nil)
classes = [BTN_SECONDARY]
classes = [ BTN_SECONDARY ]
classes << btn_size_class(size) if size
classes.join(' ')
classes.join(" ")
end

def btn_accent(size = nil)
classes = [BTN_ACCENT]
classes = [ BTN_ACCENT ]
classes << btn_size_class(size) if size
classes.join(' ')
classes.join(" ")
end

def btn_ghost(size = nil)
classes = [BTN_GHOST]
classes = [ BTN_GHOST ]
classes << btn_size_class(size) if size
classes.join(' ')
classes.join(" ")
end

def btn_danger(size = nil)
classes = [BTN_DANGER]
classes = [ BTN_DANGER ]
classes << btn_size_class(size) if size
classes.join(' ')
classes.join(" ")
end

private

def btn_size_class(size)
case size
when :sm then BTN_SM
Expand All @@ -103,4 +103,4 @@ def btn_size_class(size)
else ""
end
end
end
end
19 changes: 19 additions & 0 deletions app/services/stripe_webhook/base_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
module StripeWebhook
class BaseHandler
attr_reader :event

def initialize(event)
@event = event
end

def handle
raise NotImplementedError
end

private

def event_object
event.data.object
end
end
end
12 changes: 12 additions & 0 deletions app/services/stripe_webhook/checkout_session_completed_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
module StripeWebhook
class CheckoutSessionCompletedHandler < BaseHandler
def handle
return unless event_object.mode == "subscription"

user = User.find_by(id: event_object.metadata.user_id)
return unless user

SubscriptionService.new(user).create_subscription(event_object.subscription)
end
end
end
11 changes: 11 additions & 0 deletions app/services/stripe_webhook/payment_failed_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
module StripeWebhook
class PaymentFailedHandler < BaseHandler
def handle
subscription = Subscription.find_by(stripe_subscription_id: event_object.subscription)
return unless subscription

Rails.logger.warn "Payment failed for subscription #{subscription.id}"
# Future: send notification email to user
end
end
end
10 changes: 10 additions & 0 deletions app/services/stripe_webhook/subscription_deleted_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module StripeWebhook
class SubscriptionDeletedHandler < BaseHandler
def handle
subscription = Subscription.find_by(stripe_subscription_id: event_object.id)
return unless subscription

subscription.update!(status: "canceled")
end
end
end
14 changes: 14 additions & 0 deletions app/services/stripe_webhook/subscription_updated_handler.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module StripeWebhook
class SubscriptionUpdatedHandler < BaseHandler
def handle
subscription = Subscription.find_by(stripe_subscription_id: event_object.id)
return unless subscription

subscription.update!(
status: event_object.status,
current_period_end: Time.at(event_object.current_period_end),
cancel_at_period_end: event_object.cancel_at_period_end
)
end
end
end
83 changes: 8 additions & 75 deletions app/services/stripe_webhook_service.rb
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
class StripeWebhookService
attr_reader :event

HANDLER_MAP = {
"checkout.session.completed" => StripeWebhook::CheckoutSessionCompletedHandler,
"customer.subscription.updated" => StripeWebhook::SubscriptionUpdatedHandler,
"customer.subscription.deleted" => StripeWebhook::SubscriptionDeletedHandler,
"invoice.payment_failed" => StripeWebhook::PaymentFailedHandler
}.freeze

def initialize(event)
@event = event
end

def process
handler_class = handler_for(event.type)
handler_class = HANDLER_MAP[event.type]
return log_unhandled_event unless handler_class

handler_class.new(event).handle
Expand All @@ -17,81 +24,7 @@ def process

private

def handler_for(event_type)
case event_type
when "checkout.session.completed"
CheckoutSessionCompletedHandler
when "customer.subscription.updated"
SubscriptionUpdatedHandler
when "customer.subscription.deleted"
SubscriptionDeletedHandler
when "invoice.payment_failed"
PaymentFailedHandler
end
end

def log_unhandled_event
Rails.logger.info "Unhandled Stripe event type: #{event.type}"
end

class BaseHandler
attr_reader :event

def initialize(event)
@event = event
end

def handle
raise NotImplementedError
end

protected

def event_object
event.data.object
end
end

class CheckoutSessionCompletedHandler < BaseHandler
def handle
return unless event_object.mode == "subscription"

user = User.find_by(id: event_object.metadata.user_id)
return unless user

SubscriptionService.new(user).create_subscription(event_object.subscription)
end
end

class SubscriptionUpdatedHandler < BaseHandler
def handle
subscription = Subscription.find_by(stripe_subscription_id: event_object.id)
return unless subscription

subscription.update!(
status: event_object.status,
current_period_end: Time.at(event_object.current_period_end),
cancel_at_period_end: event_object.cancel_at_period_end
)
end
end

class SubscriptionDeletedHandler < BaseHandler
def handle
subscription = Subscription.find_by(stripe_subscription_id: event_object.id)
return unless subscription

subscription.update!(status: "canceled")
end
end

class PaymentFailedHandler < BaseHandler
def handle
subscription = Subscription.find_by(stripe_subscription_id: event_object.subscription)
return unless subscription

Rails.logger.warn "Payment failed for subscription #{subscription.id}"
# Future: send notification email to user
end
end
end
8 changes: 4 additions & 4 deletions spec/services/stripe_webhook_service_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
end

it 'delegates to CheckoutSessionCompletedHandler' do
expect_any_instance_of(StripeWebhookService::CheckoutSessionCompletedHandler)
expect_any_instance_of(StripeWebhook::CheckoutSessionCompletedHandler)
.to receive(:handle)

service.process
Expand Down Expand Up @@ -75,7 +75,7 @@
end

it 'delegates to SubscriptionUpdatedHandler' do
expect_any_instance_of(StripeWebhookService::SubscriptionUpdatedHandler)
expect_any_instance_of(StripeWebhook::SubscriptionUpdatedHandler)
.to receive(:handle)

service.process
Expand Down Expand Up @@ -125,7 +125,7 @@
end

it 'delegates to SubscriptionDeletedHandler' do
expect_any_instance_of(StripeWebhookService::SubscriptionDeletedHandler)
expect_any_instance_of(StripeWebhook::SubscriptionDeletedHandler)
.to receive(:handle)

service.process
Expand All @@ -151,7 +151,7 @@
end

it 'delegates to PaymentFailedHandler' do
expect_any_instance_of(StripeWebhookService::PaymentFailedHandler)
expect_any_instance_of(StripeWebhook::PaymentFailedHandler)
.to receive(:handle)

service.process
Expand Down
Loading