From cecd1db0702c56a9682009648da11dced55e4ae9 Mon Sep 17 00:00:00 2001 From: rhymes Date: Wed, 10 Jul 2019 22:40:56 +0200 Subject: [PATCH] Sponsorship: business logic (step 2) (#3440) * Add Sponsorship model * Add Sponsorship to admin * Add correct relationships between models * Add temporary script to migrate sponsorship data * Use constants for enums * Use Sponsorship model in PartnershipsController * Use Sponsorship in the views * Validate sponsorship levels and tag combos * Replace the rest of the sponsorship code to use Sponsorship --- app/controllers/partnerships_controller.rb | 124 ++++----- app/controllers/tags_controller.rb | 2 +- app/decorators/organization_decorator.rb | 7 - app/decorators/sponsorship_decorator.rb | 12 + app/helpers/application_helper.rb | 16 -- app/models/sponsorship.rb | 38 ++- app/models/tag.rb | 1 + app/views/articles/_sidebar.html.erb | 8 +- app/views/articles/_single_sponsor.html.erb | 7 +- app/views/articles/tags/_sidebar.html.erb | 4 +- .../_sidebar_additional.html.erb | 9 +- app/views/pages/sponsors.html.erb | 22 +- app/views/partnerships/_form.html.erb | 53 ++-- app/views/partnerships/show.html.erb | 2 +- app/views/tags/index.html.erb | 4 +- config/routes.rb | 3 +- spec/models/sponsorship_spec.rb | 47 +++- spec/requests/partnerships_spec.rb | 248 ++++++++++++++---- spec/requests/stories_index_spec.rb | 42 +-- spec/requests/user_profile_spec.rb | 4 +- 20 files changed, 438 insertions(+), 215 deletions(-) create mode 100644 app/decorators/sponsorship_decorator.rb diff --git a/app/controllers/partnerships_controller.rb b/app/controllers/partnerships_controller.rb index 058e468f6..4d4253b69 100644 --- a/app/controllers/partnerships_controller.rb +++ b/app/controllers/partnerships_controller.rb @@ -10,82 +10,82 @@ class PartnershipsController < ApplicationController def show skip_authorization + + @organizations = current_user&.admin_organizations end def create - @level = params[:sponsorship_level] - @number_of_credits_needed = credits_for_level - @organization = Organization.find(params[:organization_id]) - update_sponsorship_instructions + @organization = Organization.find(sponsorship_params[:organization_id]) authorize @organization, :admin_of_org? - @available_org_credits = @organization.credits.where(spent: false) - if @level == "tag" - @tag = Tag.find_by(name: params[:tag_name]) - @tag.sponsor_organization_id = @organization.id - slackbot_ping("@#{current_user.username} bought a ##{@tag.name} sponsorship for @#{@organization.username}") - raise "Not enough credits" unless @available_org_credits.size >= @number_of_credits_needed - - @tag.save! - spend_credits - redirect_back(fallback_location: "/partnerships") - elsif @level == "media" - # For now. Just ping slack. - slackbot_ping("📹 @#{current_user.username} bought #{@number_of_credits_needed} media credits for @#{@organization.username}") - if @available_org_credits.size >= @number_of_credits_needed - spend_credits - redirect_back(fallback_location: "/partnerships") - end - elsif @level == "devrel" - slackbot_ping("✍ @#{current_user.username} bought a devrel partnership for @#{@organization.username}") - if @available_org_credits.size >= @number_of_credits_needed - spend_credits - redirect_back(fallback_location: "/partnerships") - end - else - @organization.sponsorship_level = @level - @organization.sponsorship_status = "pending" - @organization.sponsorship_expires_at = (@organization.sponsorship_expires_at || Time.current) + 1.month - slackbot_ping("@#{current_user.username} bought a #{@level} sponsorship for @#{@organization.username}") - - raise "Not enough credits" unless @available_org_credits.size >= @number_of_credits_needed - - @organization.save! - spend_credits - redirect_back(fallback_location: "/partnerships") + @level = sponsorship_params[:sponsorship_level] + @number_of_credits_needed = Sponsorship::CREDITS[@level] + if @level == "media" && @number_of_credits_needed.nil? + @number_of_credits_needed = sponsorship_params[:sponsorship_amount].to_i end + @available_org_credits = @organization.credits.unspent + + # NOTE: this should probably be a redirect with a notice + raise "Not enough credits" unless @available_org_credits.size >= @number_of_credits_needed + + tag_sponsorship = @level == "tag" + @tag = Tag.find_by!(name: sponsorship_params[:tag_name]) if tag_sponsorship + + sponsorable = tag_sponsorship ? @tag : nil + purchase_sponsorship( + organization: @organization, + level: @level, + cost: @number_of_credits_needed, + sponsorable: sponsorable, + ) + + slackbot_ping(tag_sponsorship) + + redirect_back(fallback_location: "/partnerships") end private - def credits_for_level - if @level == "gold" - 6000 - elsif @level == "silver" - 500 - elsif @level == "tag" - 300 - elsif @level == "bronze" - 100 - elsif @level == "devrel" - 500 - elsif @level == "media" - params[:sponsorship_amount].to_i - else - raise "Invalid level" + def sponsorship_params + allowed_params = %i[organization_id sponsorship_level sponsorship_amount tag_name sponsorship_instructions] + params.permit(allowed_params) + end + + # NOTE: this should probably end up in a service object at some point + def purchase_sponsorship(organization:, level:, cost:, sponsorable: nil) + expires_at = Sponsorship::LEVELS_WITH_EXPIRATION.include?(level) ? 1.month.from_now : nil + create_params = { + user: current_user, + level: level, + status: :pending, + expires_at: expires_at + } + create_params[:sponsorable] = sponsorable if sponsorable + + if sponsorship_params[:sponsorship_instructions] + create_params[:instructions] = sponsorship_params[:sponsorship_instructions] + # NOTE: why are we storing the updated_at of the instructions? + create_params[:instructions_updated_at] = Time.current + end + + ActiveRecord::Base.transaction do + sponsorship = organization.sponsorships.create!(create_params) + + Credits::Buyer.call( + purchaser: organization, + purchase: sponsorship, + cost: cost, + ) end end - def update_sponsorship_instructions - @organization.sponsorship_instructions = @organization.sponsorship_instructions + "\n---\n#{Time.current}\n---\n" + params[:sponsorship_instructions].to_s - @organization.sponsorship_instructions_updated_at = Time.current - end + def slackbot_ping(tag_sponsorship) + text = if tag_sponsorship + "@#{current_user.username} bought a ##{@tag.name} sponsorship for @#{@organization.username}" + else + "@#{current_user.username} bought a #{@level} sponsorship for @#{@organization.username}" + end - def spend_credits - @available_org_credits.limit(@number_of_credits_needed).update_all(spent: true) - end - - def slackbot_ping(text) SlackBot.ping( text, channel: "incoming-partners", diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index 77bd761f4..dc248385d 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -6,7 +6,7 @@ class TagsController < ApplicationController def index skip_authorization @tags_index = true - @tags = Tag.includes(:sponsor_organization).order("hotness_score DESC").limit(100) + @tags = Tag.includes(:sponsorship).order(hotness_score: :desc).limit(100) end def edit diff --git a/app/decorators/organization_decorator.rb b/app/decorators/organization_decorator.rb index bf7053b3e..348f99730 100644 --- a/app/decorators/organization_decorator.rb +++ b/app/decorators/organization_decorator.rb @@ -25,11 +25,4 @@ class OrganizationDecorator < ApplicationDecorator text: "#ffffff" } end - - def sponsorship_color_hex - hexes = { "gold" => "linear-gradient(to right, #faf0e6 8%, #faf3e6 18%, #fcf6eb 33%);", - "silver" => "linear-gradient(to right, #e3e3e3 8%, #f0eded 18%, #e8e8e8 33%);", - "bronze" => "linear-gradient(to right, #ebe2d3 8%, #f5eee1 18%, #ede6d8 33%);" } - hexes[sponsorship_level] - end end diff --git a/app/decorators/sponsorship_decorator.rb b/app/decorators/sponsorship_decorator.rb new file mode 100644 index 000000000..02c083a27 --- /dev/null +++ b/app/decorators/sponsorship_decorator.rb @@ -0,0 +1,12 @@ +class SponsorshipDecorator < ApplicationDecorator + delegate_all + + def level_color_hex + hexes = { + "gold" => "linear-gradient(to right, #faf0e6 8%, #faf3e6 18%, #fcf6eb 33%);", + "silver" => "linear-gradient(to right, #e3e3e3 8%, #f0eded 18%, #e8e8e8 33%);", + "bronze" => "linear-gradient(to right, #ebe2d3 8%, #f5eee1 18%, #ede6d8 33%);" + } + hexes[level] + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 3199dbde8..435be6ee0 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -195,20 +195,4 @@ module ApplicationHelper def community_qualified_name "The #{ApplicationConfig['COMMUNITY_NAME']} Community" end - - def sponsorship_credits_price(level) - if level == "bronze" - 100 - elsif level == "silver" - 500 - elsif level == "gold" - 4000 - elsif level == "tag" - 300 - elsif level == "media" - 25 - elsif level == "devrel" - 500 - end - end end diff --git a/app/models/sponsorship.rb b/app/models/sponsorship.rb index 459566aba..aedffe1ba 100644 --- a/app/models/sponsorship.rb +++ b/app/models/sponsorship.rb @@ -1,10 +1,44 @@ class Sponsorship < ApplicationRecord + LEVELS = %w[gold silver bronze tag media devrel].freeze + LEVELS_WITH_EXPIRATION = %w[gold silver bronze].freeze + STATUSES = %w[none pending live].freeze + # media has no fixed amount of credits + CREDITS = { + gold: 6_000, + silver: 500, + bronze: 100, + tag: 300, + devrel: 500 + }.with_indifferent_access.freeze + belongs_to :user belongs_to :organization, inverse_of: :sponsorships belongs_to :sponsorable, polymorphic: true, optional: true validates :user, :organization, :featured_number, presence: true - validates :level, inclusion: { in: %w[gold silver bronze tag media devrel] } - validates :status, inclusion: { in: %w[none pending live] } + validates :level, inclusion: { in: LEVELS } + validates :status, inclusion: { in: STATUSES } validates :url, url: { allow_blank: true, no_local: true, schemes: %w[http https] } + validates :level, + uniqueness: { + scope: :organization, + message: "You can have only one sponsorship of #{LEVELS_WITH_EXPIRATION}" + }, + if: proc { LEVELS_WITH_EXPIRATION.include?(level) } + validates :level, + uniqueness: { + scope: [:sponsorable], + message: proc { "The tag is already sponsored" } + }, + if: proc { level.to_s == "tag" } + + scope :gold, -> { where(level: :gold) } + scope :silver, -> { where(level: :silver) } + scope :bronze, -> { where(level: :bronze) } + scope :tag, -> { where(level: :tag) } + scope :media, -> { where(level: :media) } + scope :devrel, -> { where(level: :devrel) } + + scope :live, -> { where(status: :live) } + scope :pending, -> { where(status: :pending) } end diff --git a/app/models/tag.rb b/app/models/tag.rb index 2b419d1d3..63e179492 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -19,6 +19,7 @@ class Tag < ActsAsTaggableOn::Tag belongs_to :badge, optional: true belongs_to :sponsor_organization, class_name: "Organization", optional: true + has_one :sponsorship, as: :sponsorable, inverse_of: :sponsorable, dependent: :destroy mount_uploader :profile_image, ProfileImageUploader mount_uploader :social_image, ProfileImageUploader diff --git a/app/views/articles/_sidebar.html.erb b/app/views/articles/_sidebar.html.erb index 26b164d0d..189722a1d 100644 --- a/app/views/articles/_sidebar.html.erb +++ b/app/views/articles/_sidebar.html.erb @@ -10,11 +10,11 @@ " alt="emoji heart" /> - <% @sponsors = Organization.where(sponsorship_level: "gold", sponsorship_status: "live").order("sponsorship_featured_number ASC") %> - <% if @sponsors.any? %> + <% @sponsorships = Sponsorship.gold.live.includes(:organization).order(featured_number: :asc) %> + <% if @sponsorships.any? %>
- <% @sponsors.each do |organization| %> - <%= render "articles/single_sponsor", organization: organization %> + <% @sponsorships.find_each do |sponsorship| %> + <%= render "articles/single_sponsor", sponsorship: sponsorship %> <% end %>
We are grateful for wonderful sponsors who help sustain the <%= ApplicationConfig["COMMUNITY_NAME"] %> community. diff --git a/app/views/articles/_single_sponsor.html.erb b/app/views/articles/_single_sponsor.html.erb index ed1c75191..45a454a5a 100644 --- a/app/views/articles/_single_sponsor.html.erb +++ b/app/views/articles/_single_sponsor.html.erb @@ -1,11 +1,12 @@ +<% organization = sponsorship.organization %>
<% end %> - <% if @tag_model.sponsor_organization_id && @tag_model.sponsorship_status == "live" %> + <% if @tag_model.sponsorship && @tag_model.sponsorship.status == "live" %>

<%= @tag %> is sponsored by

- <%= render "articles/single_sponsor", organization: @tag_model.sponsor_organization %> + <%= render "articles/single_sponsor", sponsorship: @tag_model.sponsorship %>
<% end %> diff --git a/app/views/organizations/_sidebar_additional.html.erb b/app/views/organizations/_sidebar_additional.html.erb index ac25357fa..584e2245c 100644 --- a/app/views/organizations/_sidebar_additional.html.erb +++ b/app/views/organizations/_sidebar_additional.html.erb @@ -1,17 +1,18 @@