[deploy] Remove Unused Pro Memberships (#7894)

This commit is contained in:
Molly Struve 2020-05-20 11:54:30 -05:00 committed by GitHub
parent f65d61c8db
commit 1c461f8891
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
53 changed files with 25 additions and 1590 deletions

View file

@ -40,4 +40,3 @@ linters:
- '**/app/views/pages/onboarding.html.erb'
- '**/app/views/partnerships/show.html.erb'
- '**/app/views/partnerships/index.html.erb'
- '**/app/views/pro_memberships/show.html.erb'

View file

@ -1,12 +1,10 @@
'use strict';
/* global timestampToLocalDateTime */
function initializeSettings() {
// highlights organization secret on click
const settingsOrgSecret = document.getElementById('settings-org-secret');
if (settingsOrgSecret) {
settingsOrgSecret.addEventListener('click', event => {
settingsOrgSecret.addEventListener('click', (event) => {
event.target.select();
});
}
@ -29,20 +27,4 @@ function initializeSettings() {
timeOptions,
);
}
// asks for confirmation on activating pro membership
const createProForm = document.getElementById('new_pro_membership');
if (createProForm) {
createProForm.addEventListener('submit', event => {
event.preventDefault();
// eslint-disable-next-line no-alert
if (window.confirm('Are you sure?')) {
event.target.submit();
return true;
}
return false;
});
}
}

View file

@ -1,21 +0,0 @@
module Admin
class ProMembershipsController < Admin::ApplicationController
# To customize the behavior of this controller,
# you can overwrite any of the RESTful actions. For example:
#
# def index
# super
# @resources = ProMembership.
# page(params[:page]).
# per(10)
# end
# Define a custom finder by overriding the `find_resource` method:
# def find_resource(param)
# ProMembership.find_by!(slug: param)
# end
# See https://administrate-prototype.herokuapp.com/customizing_controller_actions
# for more information
end
end

View file

@ -1,46 +0,0 @@
class ProMembershipsController < ApplicationController
before_action :authenticate_user!, except: %i[show]
before_action :load_pro_membership, only: %i[update]
after_action :verify_authorized, except: %i[show]
def show
@user = current_user
@pro_membership = current_user&.pro_membership
end
def create
authorize ProMembership
if ProMemberships::Creator.call(current_user)
flash[:settings_notice] = "You are now a Pro!"
else
flash[:error] = "You don't have enough credits!"
end
redirect_to user_settings_path("pro-membership")
end
def update
raise Pundit::NotAuthorizedError, "You don't have a Pro Membership" unless @pro_membership
authorize @pro_membership
if @pro_membership.update(update_params)
flash[:settings_notice] = "Your membership has been updated!"
else
flash[:error] = "An error has occurred while updating your membership!"
end
redirect_to user_settings_path("pro-membership")
end
private
def load_pro_membership
@pro_membership = current_user.pro_membership
end
def update_params
params.require(:pro_membership).permit(:auto_recharge)
end
end

View file

@ -253,8 +253,6 @@ class UsersController < ApplicationController
handle_integrations_tab
when "billing"
handle_billing_tab
when "pro-membership"
handle_pro_membership_tab
when "account"
handle_account_tab
when "response-templates"
@ -323,10 +321,6 @@ class UsersController < ApplicationController
@customer = Payments::Customer.get(stripe_code) if stripe_code.present?
end
def handle_pro_membership_tab
@pro_membership = current_user.pro_membership
end
def handle_account_tab
community_name = ApplicationConfig["COMMUNITY_NAME"]
@email_body = <<~HEREDOC

View file

@ -29,7 +29,6 @@ class DashboardManifest
html_variant_trials
html_variant_successes
sponsorships
pro_memberships
classified_listing_categories
].freeze
# DASHBOARDS = [

View file

@ -1,67 +0,0 @@
require "administrate/base_dashboard"
class ProMembershipDashboard < Administrate::BaseDashboard
# ATTRIBUTE_TYPES
# a hash that describes the type of each of the model's fields.
#
# Each different type represents an Administrate::Field object,
# which determines how the attribute is displayed
# on pages throughout the dashboard.
ATTRIBUTE_TYPES = {
user: Field::BelongsTo,
id: Field::Number,
status: Field::String,
expires_at: Field::DateTime,
expiration_notification_at: Field::DateTime,
expiration_notifications_count: Field::Number,
auto_recharge: Field::Boolean,
created_at: Field::DateTime,
updated_at: Field::DateTime
}.freeze
# COLLECTION_ATTRIBUTES
# an array of attributes that will be displayed on the model's index page.
#
# By default, it's limited to four items to reduce clutter on index pages.
# Feel free to add, remove, or rearrange items.
COLLECTION_ATTRIBUTES = %i[
user
id
status
expires_at
auto_recharge
].freeze
# SHOW_PAGE_ATTRIBUTES
# an array of attributes that will be displayed on the model's show page.
SHOW_PAGE_ATTRIBUTES = %i[
user
id
status
expires_at
expiration_notification_at
expiration_notifications_count
auto_recharge
created_at
updated_at
].freeze
# FORM_ATTRIBUTES
# an array of attributes that will be displayed
# on the model's form (`new` and `edit`) pages.
FORM_ATTRIBUTES = %i[
user
status
expires_at
expiration_notification_at
expiration_notifications_count
auto_recharge
].freeze
# Overwrite this method to customize how pro memberships are displayed
# across all pages of the admin dashboard.
#
# def display_resource(pro_membership)
# "ProMembership ##{pro_membership.id}"
# end
end

View file

@ -53,7 +53,6 @@ class UserDecorator < ApplicationDecorator
body_class = [
config_theme.tr("_", "-"),
"#{config_font.tr('_', '-')}-article-body",
"pro-status-#{pro?}",
"trusted-status-#{trusted}",
"#{config_navbar.tr('_', '-')}-navbar-config",
]

View file

@ -31,7 +31,7 @@ class ArticleSuggester
Article.published.where(featured: true).
where.not(id: ids_to_ignore).
order("hotness_score DESC").
includes(user: [:pro_membership]).
includes(:user).
offset(rand(0..offsets[1])).
first(max)
end
@ -40,7 +40,7 @@ class ArticleSuggester
Article.published.tagged_with(article.tag_list, any: true).
where.not(id: article.id).
order("hotness_score DESC").
includes(user: [:pro_membership]).
includes(:user).
offset(rand(0..offsets[0])).
first(max)
end

View file

@ -1,11 +0,0 @@
class ProMembershipMailer < ApplicationMailer
default from: -> { email_from("Pro Memberships") }
def expiring_membership(pro_membership, expiration_date)
@pro_membership = pro_membership
@user = pro_membership.user
@days = (Time.current.to_date..expiration_date.to_date).count - 1
@date = expiration_date.to_date
mail(to: @user.email, subject: "Your Pro Membership will expire in #{@days} days!")
end
end

View file

@ -551,8 +551,7 @@ class Article < ApplicationRecord
username: object.username,
slug: object == organization ? object.slug : object.username,
profile_image_90: object.profile_image_90,
profile_image_url: object.profile_image_url,
pro: object == user ? user.pro? : false # organizations can't be pro users
profile_image_url: object.profile_image_url
}
end

View file

@ -99,10 +99,6 @@ class Organization < ApplicationRecord
credits.unspent.size >= num_credits_needed
end
def pro?
false
end
def banned
false
end

View file

@ -1,61 +0,0 @@
class ProMembership < ApplicationRecord
STATUSES = %w[active expired].freeze
MONTHLY_COST = 5
MONTHLY_COST_USD = 25
belongs_to :user
validates :user, :status, :expiration_notifications_count, presence: true
validates :user, uniqueness: true
validates :expires_at, presence: true, on: :save
validates :status, inclusion: { in: STATUSES }
scope :active, -> { where(status: :active) }
scope :expired, -> { where(status: :expired) }
before_create :set_expiration_date
after_save :resave_user_articles
after_save :bust_cache
def expired?
expires_at <= Time.current
end
def active?
expires_at > Time.current
end
def expire!
update!(expires_at: Time.current, status: :expired)
end
def renew!
update!(
expires_at: 1.month.from_now,
status: :active,
expiration_notification_at: nil,
expiration_notifications_count: 0,
)
end
private
def set_expiration_date
self.expires_at = 1.month.from_now
end
# if the membership is new or the user flips from expired to active and viceversa,
# we need to resave all of the user's articles to make sure that the pro details
# that are cached with them are refreshed
def resave_user_articles
if saved_change_to_created_at? ||
saved_change_to_expires_at? ||
saved_change_to_status?
Users::ResaveArticlesWorker.perform_async(user.id)
end
end
def bust_cache
Rails.cache.delete("user-#{user.id}/has_pro_membership")
end
end

View file

@ -92,7 +92,6 @@ class User < ApplicationRecord
has_many :webhook_endpoints, class_name: "Webhook::Endpoint", foreign_key: :user_id, inverse_of: :user, dependent: :delete_all
has_one :counters, class_name: "UserCounter", dependent: :destroy
has_one :pro_membership, dependent: :destroy
mount_uploader :profile_image, ProfileImageUploader
@ -292,8 +291,8 @@ class User < ApplicationRecord
end
def pro?
Rails.cache.fetch("user-#{id}/has_pro_membership", expires_in: 200.hours) do
pro_membership&.active? || has_role?(:pro)
Rails.cache.fetch("user-#{id}/has_pro_role", expires_in: 200.hours) do
has_role?(:pro)
end
end

View file

@ -1,9 +0,0 @@
class ProMembershipPolicy < ApplicationPolicy
def create?
user.pro_membership.nil? && !user.has_role?(:pro)
end
def update?
user.pro_membership.present?
end
end

View file

@ -1,118 +0,0 @@
module ProMemberships
# Bills pro memberships that expire today
class Biller
def self.call(*args)
new(*args).call
end
def call
relation = ProMembership.includes(user: [:credits]).
where("DATE(expires_at) = ?", Time.zone.today)
relation.find_each do |membership|
user = membership.user
cost = ProMembership::MONTHLY_COST
if user.enough_credits?(cost)
renew_membership(membership, cost)
elsif membership.auto_recharge
if user.stripe_id_code
charge_user_and_renew_membership(membership, cost)
else
notify_admins(user, "auto recharge error: missing Stripe customer ID!")
end
else
expire_membership(membership)
notify_admins(user, "pro membership expired!")
end
end
end
private
def renew_membership(membership, cost)
ActiveRecord::Base.transaction do
user = membership.user
membership.renew!
success = Credits::Buyer.call(
purchaser: user,
purchase: membership,
cost: cost,
)
unless success
notify_admins("#{user.name}'s pro membership could not be renewed with enough credits!")
raise ActiveRecord::Rollback
end
chat_channel = ChatChannel.find_by(slug: "pro-members")
if chat_channel && !chat_channel.chat_channel_memberships.exists?(user_id: user.id)
chat_channel.add_users(user)
end
success
end
rescue StandardError => e
Rails.logger.error(e)
notify_admins(membership.user, "error: #{e.message}")
end
def expire_membership(membership)
ActiveRecord::Base.transaction do
user = membership.user
membership.expire!
chat_channel = ChatChannel.find_by(slug: "pro-members")
chat_channel&.remove_user(user)
user.save
end
rescue StandardError => e
Rails.logger.error(e)
notify_admins(membership.user, "error: #{e.message}")
end
def recharge(membership, cost)
user = membership.user
# charge customer for credits
customer = Payments::Customer.get(user.stripe_id_code)
Payments::Customer.charge(
customer: customer,
amount: ProMembership::MONTHLY_COST_USD,
description: "Purchase of #{cost} credits.",
)
# add credits
credits = Array.new(cost) { Credit.new(user: user, cost: 1) }
user.credits << credits
user.save!
end
def charge_user_and_renew_membership(membership, cost)
user = membership.user
recharge(membership, cost)
renew_membership(membership, cost)
rescue Payments::PaymentsError => e
Rails.logger.error(e)
notify_admins(user, "payment error: #{e.message}")
rescue ActiveRecord::RecordInvalid => e
Rails.logger.error(e)
notify_admins(user, "credits creation error: #{e.message}")
rescue StandardError => e
Rails.logger.error(e)
notify_admins(user, "error: #{e.message}")
end
def notify_admins(user, message)
Slack::Messengers::Worker.perform_async(
message: "ProMemberships::Biller: #{user.username}: #{message}",
channel: "pro-memberships",
username: "pro-memberships",
icon_emoji: ":fire:",
)
end
end
end

View file

@ -1,40 +0,0 @@
module ProMemberships
class Creator
def initialize(user)
@user = user
end
def self.call(*args)
new(*args).call
end
def call
if purchase_pro_membership
channel = ChatChannel.find_by(slug: "pro-members")
channel&.add_users(user)
true
else
false
end
end
private
attr_reader :user
def purchase_pro_membership
cost = ProMembership::MONTHLY_COST
return false unless user.enough_credits?(cost)
ActiveRecord::Base.transaction do
pro_membership = ProMembership.create!(user: user)
Credits::Buyer.call(
purchaser: user,
purchase: pro_membership,
cost: cost,
)
end
end
end
end

View file

@ -1,47 +0,0 @@
module ProMemberships
# Notifies Pro members with insufficient credits of the impeding expiration
class ExpirationNotifier
def initialize(expiration_date)
@expiration_date = expiration_date
end
def self.call(*args)
new(*args).call
end
def call
count = 0
# NOTE: naive implementation because this is supposed to be called every 24hrs
relation = ProMembership.
includes(user: [:credits]).
where("DATE(expires_at) = ?", expiration_date).
where(auto_recharge: false)
relation.find_each do |membership|
next if membership.user.enough_credits?(ProMembership::MONTHLY_COST)
# NOTE: maybe we should "deliver_later" and update the flags there
ProMembershipMailer.expiring_membership(membership, expiration_date).deliver_now
membership.expiration_notification_at = Time.current
membership.increment(:expiration_notifications_count)
membership.save!
Slack::Messengers::Worker.perform_async(
message: "#{membership.user.name}'s pro membership expires on #{expiration_date}",
channel: "pro-memberships",
username: "pro-memberships",
icon_emoji: ":fire:",
)
count += 1
end
count
end
private
attr_reader :expiration_date
end
end

View file

@ -10,7 +10,7 @@ module Suggester
end
def suggest
base_articles = Article.includes(user: [:pro_membership]).
base_articles = Article.includes(:user).
includes(:organization).
where.not(id: not_ids, organization_id: nil).
cached_tagged_with(tag)

View file

@ -24,7 +24,7 @@ module Suggester
tag_name = tag_names.sample
Rails.cache.fetch("classic-article-for-tag-#{tag_name}}", expires_in: 90.minutes) do
Article.published.cached_tagged_with(tag_name).
includes(user: [:pro_membership]).
includes(:user).
limited_column_select.
where(featured: true).
where("positive_reactions_count > ?", MIN_REACTION_COUNT).

View file

@ -9,7 +9,7 @@ module Suggester
def suggest(num)
Article.published.where(featured: true).
includes(user: [:pro_membership]).
includes(:user).
limited_column_select.
where("positive_reactions_count > ?", MIN_HQ_REACTION_COUNT).
where.not(id: @not_ids).

View file

@ -21,7 +21,6 @@ module Users
user.page_views.delete_all
user.poll_skips.delete_all
user.poll_votes.delete_all
user.pro_membership.delete if user.pro_membership.present?
user.rating_votes.delete_all
user.response_templates.delete_all
user.classified_listings.destroy_all

View file

@ -5,7 +5,6 @@
<a href="<%= article.user.path + article.decorate.internal_utm_params %>">
<img class="profile-pic" src="<%= ProfileImage.new(article.user).get(width: 50) %>" alt="<%= article.user.username %> profile image" />
<span><%= article.user.name %></span>
<%= render "shared/pro_checkmark" if article.user.pro? %>
</a>
</div>
<p>

View file

@ -30,7 +30,6 @@
<a href="<%= followable.path + article.decorate.internal_utm_params %>"
class="<%= "partner-link" if classification == "boosted" %>"
data-details="<%= followable&.slug if classification == "boosted" %>__PROFILE"><%= followable.name %></a>
<%= render "shared/pro_checkmark" if followable.pro? %>
</div>
<% end %>
<button class="cta follow-action-button user-profile-follow-button <%= "partner-link" if classification == "boosted" %>"

View file

@ -19,7 +19,7 @@
<div class="content">
<h3><%= article.title %></h3>
<h4>
<%= article.user.name %><%= render "shared/pro_checkmark", width: "12px" if article.user.pro? %> - <time datetime="<%= article.published_timestamp %>"><%= article.readable_publish_date %></time>
<%= article.user.name %> - <time datetime="<%= article.published_timestamp %>"><%= article.readable_publish_date %></time>
</h4>
</div>
</div>

View file

@ -32,7 +32,6 @@
<p>
<a href="/<%= story.cached_user.username %>" class="crayons-story__secondary fw-medium">
<%= story.cached_user.name %>
<%= render "shared/pro_checkmark" if story.cached_user.pro %>
</a>
<% if story.cached_organization && !@organization_article_index %>
<span>

View file

@ -41,7 +41,6 @@
<div class="primary-sticky-nav-author-name">
<a href="<%= @actor.path %>">
<%= @actor.name %>
<%= render "shared/pro_checkmark" if @actor.pro? %>
</a>
</div>
<div class="primary-sticky-nav-author-username">

View file

@ -205,7 +205,7 @@
<% end %>
</div>
<% stick_nav_cache_key = "sticky-sidebar-#{@article.id}-#{(@organization || @user).profile_updated_at}-#{(@organization || @user).last_article_at}-#{@variant_number}-#{(@organization || @user).pro?}" %>
<% stick_nav_cache_key = "sticky-sidebar-#{@article.id}-#{(@organization || @user).profile_updated_at}-#{(@organization || @user).last_article_at}-#{@variant_number}-#{@user.pro?}" %>
<% cache(stick_nav_cache_key, expires_in: 50.hours) do %>
<%= render "articles/sticky_nav" %>
<% end %>

View file

@ -1,3 +0,0 @@
<a href="<%= pro_membership_path %>">
Pro Membership
</a>

View file

@ -1,8 +0,0 @@
<% credits_url = app_url(credits_path) %>
<% pro_membership_url = app_url(user_settings_path("pro-membership")) %>
<p>Hi <%= @user.name %>, your Pro Membership will expire in <%= @days %> days on <%= @date.to_s(:long) %>!</p>
<p>Make sure you have enough credits for its renewal: <a href="<%= credits_url %>"><%= credits_url %></a>.</p>
<p>You can also <a href="<%= pro_membership_url %>">review your Pro Membership status</a>.</p>

View file

@ -1,8 +0,0 @@
<% credits_url = app_url(credits_path) %>
<% pro_membership_url = app_url(user_settings_path("pro-membership")) %>
Hi <%= @user.name %>, your Pro Membership will expire in <%= @days %> days on <%= @date.to_s(:long) %>!
Make sure you have enough credits for its renewal: <%= credits_url %>
You can also review your Pro Membership status: <%= pro_membership_url %>

View file

@ -1,51 +0,0 @@
<% title "Pro Membership" %>
<style>
<% cache(cache_key_heroku_slug("partnership-org-section-credits"), expires_in: 10.hours) do %>
<%= Rails.application.assets["partnerships.css"].to_s.html_safe %>
<% end %>
</style>
<div class="partnerships-container">
<% if flash[:notice].present? %>
<div class="notice" id="notice">
<%= flash[:notice] %>
</div>
<% elsif flash[:error].present? %>
<div class="notice error-notice" id="notice" style="text-align:center;color:red">
ERROR
<%= flash[:error] %>
</div>
<% end %>
<h1><img src="<%= asset_path "rainbowdev.svg" %>" /> Like a Pro</h1>
<p>
<strong><em>Pro is coming soon. This page is a work in progress. Since we're open source and transparently working on this, no sense in keeping it private eh?</em></strong>
</p>
<p>
<strong><em>The copy on this page is a total draft. No judgment yet pls. 😄</em></strong>
</p>
<h3>🚀 Access the Bleeding Edge</h3>
<p>A pro membership gives you access to <em>bleeding edge</em> features that may some day scale to general availability. However, we would rather launch great new features to a core dedicated userbase that is most interested than to delay until things are scalable to the whole community.
<p><strong>Current Bleeding Edge Features</strong></p>
<ul>
<li>Pro Analytics: Deeper insights into the performance of your posts, traffic sources, etc.</li>
<li>Searchable Pro History: Instant access to a repository of your entire reading history—for your own heightened reference.</li>
</ul>
<h3>👋 Access the Exclusive Areas</h3>
<p>A pro membership gives you access to small pro groups that don't necessarily scale. Your pro membership immediately gives you access to <em>Editorial Groups</em> in <%= community_name %> Connect. These are chat channels dedicated to peer-to-peer review of technical blog posts to help you make the most of your <%= community_name %> presence.
<h3>🤝 Access the Trust-based features</h3>
<p>While pro memberships will not give you preferential treatment in the feed algorithm (that would be a no-no), you will get skin-in-the-game based access to trust-based features where we can place your posts more quickly and effectively in areas where spam would otherwise inhibit us.
<h3>🙌 Always Improving</h3>
<p>Because this will be the landing destination for experimental and low scale features, you will always have the best and most interesting <%= community_name %> features. We also plan to connect for exclusive deals on new and upcoming features on other platforms.
<% if @user %>
<div class="partner-credits-explainer">
<% if @user.pro? %>
<%= link_to "View your Pro Membership", user_settings_path("pro-membership") %>
<% else %>
<%= link_to "Become a Pro member", user_settings_path("pro-membership") %>
<% end %>
</div>
<% end %>
</div>

View file

@ -1 +0,0 @@
<% # placeholder for future "pro indicator" %>

View file

@ -1,66 +0,0 @@
<h3>Pro Membership</h3>
<% if @user.pro? %>
<% if @pro_membership %>
<%= form_for(@pro_membership) do |f| %>
<dl class="membership-status">
<dt>Status</dt>
<dd class="status">
<% if @pro_membership.active? %>
<%= image_tag("checkmark-green.svg", class: "icon-img", alt: "check mark", title: "active", width: 25) %>
<% else %>
<span class="no">expired</span>
<% end %>
</dd>
<dt>Expiration date</dt>
<dd class="expiration-date">
<time datetime="<%= @pro_membership.expires_at.iso8601 %>">
<%= @pro_membership.expires_at.to_date.to_s(:long) %>
</time>
</dd>
<dt>Top up from credit card?</dt>
<dd class="auto-recharge">
<div class="field">
<%= f.check_box :auto_recharge %>
</div>
</dd>
</dl>
<%= f.submit "SUBMIT", class: "cta" %>
<% end %>
<% else %> <!-- existing pro roles without paid membership -->
<dl class="membership-status">
<dt>Status</dt>
<dd class="status">
<%= image_tag("checkmark-green.svg", class: "icon-img", alt: "check mark", title: "active", width: 25) %>
</dd>
<dt>Expiration date</dt>
<dd class="expiration-date">
<em>Never</em>
</dd>
</dl>
<% end %>
<% else %>
<p>
<%= link_to "Pro Membership", pro_membership_path %>
is <strong><%= ProMembership::MONTHLY_COST %></strong> credits per month.
</p>
<% available_credits = @user.credits.unspent.size %>
<% if available_credits >= ProMembership::MONTHLY_COST %>
<p>You currently have <strong><%= available_credits %></strong> available credits.</p>
<%= form_for(ProMembership.new(user: @user)) do |f| %>
<%= f.submit("Become a Pro member", class: "cta") %>
<% end %>
<% elsif available_credits.positive? %>
<p>You currently have <strong>4 credits</strong>, you need <%= ProMembership::MONTHLY_COST %> to become a Pro member.</p>
<a href="<%= purchase_credits_path %>" class="membership-purchase-link">Purchase credits</a>
<% else %>
<p>You currently have <strong>no credits</strong>, you need <%= ProMembership::MONTHLY_COST %> to become a Pro member.</p>
<a href="<%= purchase_credits_path %>" class="membership-purchase-link">Purchase credits</a>
<% end %>
<% end %>
<p>
You can find additional info on the <%= link_to "Pro Membership page", pro_membership_path %>.
</p>

View file

@ -36,7 +36,7 @@
</div>
<div class="profile-details">
<h1>
<span><%= @user.name %><%= render "shared/pro_checkmark", style: nil, width: nil %></span>
<span><%= @user.name %></span>
<span class="user-profile-follow-button-wrapper">
<button id="user-follow-butt" class="cta follow-action-button user-profile-follow-button" style="color:<%= user_colors(@user)[:text] %>;background-color:<%= user_colors(@user)[:bg] %>" data-info='{"id":<%= @user.id %>,"className":"<%= @user.class.name %>"}'>&nbsp;</button>
</span>

View file

@ -223,12 +223,10 @@ Rails.application.routes.draw do
resources :partnerships, only: %i[index create show], param: :option
resources :display_ad_events, only: [:create]
resources :badges, only: [:index]
resource :pro_membership, path: :pro, only: %i[show create update]
resources :user_blocks, param: :blocked_id, only: %i[show create destroy]
resources :podcasts, only: %i[new create]
resources :article_approvals, only: %i[create]
resources :video_chats, only: %i[show]
resolve("ProMembership") { [:pro_membership] } # see https://guides.rubyonrails.org/routing.html#using-resolve
namespace :followings, defaults: { format: :json } do
get :users
get :tags

View file

@ -1,19 +0,0 @@
# All of these tasks need to be scheduled daily
namespace :pro_memberships do
desc "Notify pro users with insufficient credits that their membership is about to expire in a week"
task notify_expirations_one_week_before: :environment do
num_notified = ProMemberships::ExpirationNotifier.call(1.week.from_now)
Rails.logger.info("Notified #{num_notified} Pro users...")
end
desc "Notify pro users with insufficient credits that their membership is about to expire in a day"
task notify_expirations_one_day_before: :environment do
num_notified = ProMemberships::ExpirationNotifier.call(1.day.from_now)
Rails.logger.info("Notified #{num_notified} Pro users...")
end
desc "Bill pro users and optionally charge their cards"
task bill_users: :environment do
ProMemberships::Biller.call
end
end

View file

@ -108,7 +108,7 @@ RSpec.describe UserDecorator, type: :decorator do
describe "#config_body_class" do
it "creates proper body class with defaults" do
expected_result = %W[
default default-article-body pro-status-#{user.pro?}
default default-article-body
trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
@ -117,7 +117,7 @@ RSpec.describe UserDecorator, type: :decorator do
it "creates proper body class with sans serif config" do
user.config_font = "sans_serif"
expected_result = %W[
default sans-serif-article-body pro-status-#{user.pro?}
default sans-serif-article-body
trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
@ -126,7 +126,7 @@ RSpec.describe UserDecorator, type: :decorator do
it "creates proper body class with night theme" do
user.config_theme = "night_theme"
expected_result = %W[
night-theme default-article-body pro-status-#{user.pro?}
night-theme default-article-body
trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
@ -135,7 +135,7 @@ RSpec.describe UserDecorator, type: :decorator do
it "creates proper body class with pink theme" do
user.config_theme = "pink_theme"
expected_result = %W[
pink-theme default-article-body pro-status-#{user.pro?}
pink-theme default-article-body
trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
@ -144,7 +144,7 @@ RSpec.describe UserDecorator, type: :decorator do
it "creates proper body class with minimal light theme" do
user.config_theme = "minimal_light_theme"
expected_result = %W[
minimal-light-theme default-article-body pro-status-#{user.pro?}
minimal-light-theme default-article-body
trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
@ -153,7 +153,7 @@ RSpec.describe UserDecorator, type: :decorator do
it "works with static navbar" do
user.config_navbar = "static"
expected_result = %W[
default default-article-body pro-status-#{user.pro?}
default default-article-body
trusted-status-#{user.trusted} static-navbar-config
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
@ -162,21 +162,11 @@ RSpec.describe UserDecorator, type: :decorator do
context "when user with roles" do
let(:user) { create(:user) }
it "creates proper body class with pro user" do
user.add_role(:pro)
expected_result = %W[
default default-article-body pro-status-true
trusted-status-#{user.trusted} default-navbar-config
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)
end
it "creates proper body class with trusted user" do
user.add_role(:trusted)
expected_result = %W[
default default-article-body pro-status-#{user.pro?}
expected_result = %w[
default default-article-body
trusted-status-true default-navbar-config
].join(" ")
expect(user.decorate.config_body_class).to eq(expected_result)

View file

@ -124,12 +124,6 @@ FactoryBot.define do
end
end
trait :with_pro_membership do
after(:create) do |user|
create(:pro_membership, user: user)
end
end
trait :tag_moderator do
after(:create) do |user|
tag = create(:tag)

View file

@ -1,7 +0,0 @@
# Preview all emails at http://localhost:3000/rails/mailers/pro_membership_mailer
class ProMembershipMailerPreview < ActionMailer::Preview
def expiring_membership
pro_membership = ProMembership.find_or_create_by(user: User.last)
ProMembershipMailer.expiring_membership(pro_membership, 1.week.from_now)
end
end

View file

@ -1,69 +0,0 @@
require "rails_helper"
RSpec.describe ProMembershipMailer, type: :mailer do
let(:pro_membership) { build_stubbed(:pro_membership) }
let(:user) { pro_membership.user }
describe "#expiring_membership" do
it "works correctly" do
Timecop.freeze(Time.current) do
email = described_class.expiring_membership(pro_membership, 1.week.from_now)
expect(email.subject).to eq("Your Pro Membership will expire in 7 days!")
expect(email.to).to eq([user.email])
expect(email.from).to eq([SiteConfig.email_addresses[:default]])
expect(email["from"].value).to eq("#{ApplicationConfig['COMMUNITY_NAME']} Pro Memberships <#{SiteConfig.email_addresses[:default]}>")
end
end
context "when generating the plain text email" do
it "includes the user's name" do
email = described_class.expiring_membership(pro_membership, 1.week.from_now)
expect(email.text_part.body).to include(user.name)
end
it "includes the expiration date" do
Timecop.freeze(Time.current) do
expiration_date = 1.week.from_now
email = described_class.expiring_membership(pro_membership, expiration_date)
expect(email.text_part.body).to include(expiration_date.to_date.to_s(:long))
end
end
it "includes credits path" do
email = described_class.expiring_membership(pro_membership, 1.week.from_now)
expect(email.text_part.body).to include(credits_path)
end
it "includes pro membership path" do
email = described_class.expiring_membership(pro_membership, 1.week.from_now)
expect(email.text_part.body).to include(user_settings_path("pro-membership"))
end
end
context "when generating the html email" do
it "includes the user's name" do
email = described_class.expiring_membership(pro_membership, 1.week.from_now)
expect(email.html_part.body).to include(user.name)
end
it "includes the expiration date" do
Timecop.freeze(Time.current) do
expiration_date = 1.week.from_now
email = described_class.expiring_membership(pro_membership, expiration_date)
expect(email.html_part.body).to include(expiration_date.to_date.to_s(:long))
end
end
it "includes credits path" do
email = described_class.expiring_membership(pro_membership, 1.week.from_now)
expect(email.html_part.body).to include(CGI.escape(credits_path))
end
it "includes pro membership path" do
email = described_class.expiring_membership(pro_membership, 1.week.from_now)
expect(email.html_part.body).to include(CGI.escape(user_settings_path("pro-membership")))
end
end
end
end

View file

@ -663,7 +663,6 @@ RSpec.describe Article, type: :model do
expect(article.cached_user.slug).to eq(article.user.username)
expect(article.cached_user.profile_image_90).to eq(article.user.profile_image_90)
expect(article.cached_user.profile_image_url).to eq(article.user.profile_image_url)
expect(article.cached_user.pro).to eq(article.user.pro?)
end
it "assigns cached_organization on save" do
@ -673,7 +672,6 @@ RSpec.describe Article, type: :model do
expect(article.cached_organization.slug).to eq(article.organization.slug)
expect(article.cached_organization.profile_image_90).to eq(article.organization.profile_image_90)
expect(article.cached_organization.profile_image_url).to eq(article.organization.profile_image_url)
expect(article.cached_organization.pro).to be(false)
end
end

View file

@ -270,12 +270,6 @@ RSpec.describe Organization, type: :model do
end
end
describe "#pro?" do
it "always returns false" do
expect(build(:organization).pro?).to be(false)
end
end
describe "#decoarated" do
it "returns not fully banished" do
expect(organization.decorate.fully_banished?).to eq(false)

View file

@ -1,138 +0,0 @@
require "rails_helper"
RSpec.describe ProMembership, type: :model do
it { is_expected.to belong_to(:user) }
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:status) }
it { is_expected.to validate_presence_of(:expires_at).on(:save) }
it { is_expected.to validate_presence_of(:expiration_notifications_count) }
it { is_expected.to validate_inclusion_of(:status).in_array(ProMembership::STATUSES) }
it { is_expected.to have_db_index(:status) }
it { is_expected.to have_db_index(:expires_at) }
describe "constants" do
it "has the correct values for constants" do
expect(ProMembership::STATUSES).to eq(%w[active expired])
expect(ProMembership::MONTHLY_COST).to eq(5)
expect(ProMembership::MONTHLY_COST_USD).to eq(25)
end
end
describe "defaults" do
it "has the correct defaults" do
pm = described_class.new
expect(pm.status).to eq("active")
expect(pm.expires_at).to be(nil)
expect(pm.expiration_notification_at).to be(nil)
expect(pm.expiration_notifications_count).to eq(0)
expect(pm.auto_recharge).to be(false)
end
end
describe "creation" do
it "sets expires_at to a month from now" do
Timecop.freeze(Time.current) do
pm = described_class.create!(user: create(:user))
expect(pm.expires_at.to_i).to eq(1.month.from_now.to_i)
end
end
end
describe "#expired?" do
it "returns false if expires_at is in the future" do
Timecop.freeze(Time.current) do
pm = build(:pro_membership, expires_at: 5.seconds.from_now)
expect(pm.expired?).to be(false)
end
end
it "returns true if expires_at is in the past" do
Timecop.freeze(Time.current) do
pm = build(:pro_membership, expires_at: 5.seconds.ago)
expect(pm.expired?).to be(true)
end
end
end
describe "#active?" do
it "returns true if expires_at is in the future" do
Timecop.freeze(Time.current) do
pm = build(:pro_membership, expires_at: 5.seconds.from_now)
expect(pm.active?).to be(true)
end
end
it "returns false if expires_at is in the past" do
Timecop.freeze(Time.current) do
pm = build(:pro_membership, expires_at: 5.seconds.ago)
expect(pm.active?).to be(false)
end
end
end
describe "#expire!" do
let(:pro_membership) { create(:pro_membership) }
it "sets expires_at to the current time" do
Timecop.freeze(Time.current) do
pro_membership.expire!
expect(pro_membership.reload.expires_at.to_i).to eq(Time.current.to_i)
end
end
it "sets status to expired" do
Timecop.freeze(Time.current) do
pro_membership.expire!
expect(pro_membership.reload.status).to eq("expired")
end
end
it "makes the membership expired" do
Timecop.freeze(Time.current) do
pro_membership.expire!
expect(pro_membership.reload.expired?).to be(true)
end
end
end
describe "#renew!" do
let(:pro_membership) { create(:pro_membership) }
it "sets expires_at to a month from now" do
Timecop.freeze(Time.current) do
pro_membership.renew!
expect(pro_membership.reload.expires_at.to_i).to eq(1.month.from_now.to_i)
end
end
it "sets status to active" do
Timecop.freeze(Time.current) do
pro_membership.renew!
expect(pro_membership.reload.status).to eq("active")
end
end
it "sets expiration_notification_at to nil" do
Timecop.freeze(Time.current) do
pro_membership.update_column(:expiration_notification_at, Time.current)
pro_membership.renew!
expect(pro_membership.reload.expiration_notification_at).to be(nil)
end
end
it "sets expiration_notifications_count to 0" do
Timecop.freeze(Time.current) do
pro_membership.update_column(:expiration_notifications_count, 1)
pro_membership.renew!
expect(pro_membership.reload.expiration_notifications_count).to eq(0)
end
end
it "makes the membership active" do
Timecop.freeze(Time.current) do
pro_membership.renew!
expect(pro_membership.reload.active?).to be(true)
end
end
end
end

View file

@ -141,7 +141,6 @@ RSpec.describe User, type: :model do
end
it { is_expected.to have_one(:counters).class_name("UserCounter").dependent(:destroy) }
it { is_expected.to have_one(:pro_membership).dependent(:destroy) }
it { is_expected.not_to allow_value("#xyz").for(:bg_color_hex) }
it { is_expected.not_to allow_value("#xyz").for(:text_color_hex) }
it { is_expected.not_to allow_value("AcMe_1%").for(:username) }
@ -975,7 +974,7 @@ RSpec.describe User, type: :model do
describe "theming properties" do
it "creates proper body class with defaults" do
expect(user.decorate.config_body_class).to eq("default default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
expect(user.decorate.config_body_class).to eq("default default-article-body trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
end
it "determines dark theme if night theme" do
@ -995,22 +994,22 @@ RSpec.describe User, type: :model do
it "creates proper body class with sans serif config" do
user.config_font = "sans_serif"
expect(user.decorate.config_body_class).to eq("default sans-serif-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
expect(user.decorate.config_body_class).to eq("default sans-serif-article-body trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
end
it "creates proper body class with open dyslexic config" do
user.config_font = "open_dyslexic"
expect(user.decorate.config_body_class).to eq("default open-dyslexic-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
expect(user.decorate.config_body_class).to eq("default open-dyslexic-article-body trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
end
it "creates proper body class with night theme" do
user.config_theme = "night_theme"
expect(user.decorate.config_body_class).to eq("night-theme default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
expect(user.decorate.config_body_class).to eq("night-theme default-article-body trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
end
it "creates proper body class with pink theme" do
user.config_theme = "pink_theme"
expect(user.decorate.config_body_class).to eq("pink-theme default-article-body pro-status-#{user.pro?} trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
expect(user.decorate.config_body_class).to eq("pink-theme default-article-body trusted-status-#{user.trusted} #{user.config_navbar}-navbar-config")
end
end
@ -1068,19 +1067,6 @@ RSpec.describe User, type: :model do
user.add_role(:pro)
expect(user.pro?).to be(true)
end
it "returns true if the user has an active pro membership" do
user.pro_membership = build(:pro_membership, status: "active")
expect(user.pro?).to be(true)
end
it "returns false if the user has an expired pro membership" do
Timecop.freeze(Time.current) do
membership = create(:pro_membership, user: user)
membership.expire!
expect(user.pro?).to be(false)
end
end
end
describe "#enough_credits?" do

View file

@ -96,19 +96,6 @@ RSpec.describe "Credits", type: :request do
expect(response.body).to include("Purchase history")
expect(response.body).to include("Miscellaneous items")
end
it "shows a pro membership purchase" do
pro_membership = create(:pro_membership, user: user)
purchase_params = { user: user, purchase_type: pro_membership.class.name, purchase_id: pro_membership.id }
create(:credit, params.merge(purchase_params))
sign_in user
get credits_path
expect(response.body).to include("Purchase history")
expect(response.body).to include("Pro Membership")
expect(response.body).to include(pro_membership_path)
end
end
end

View file

@ -216,15 +216,6 @@ RSpec.describe "Dashboards", type: :request do
end
end
context "when user has a pro membership" do
it "shows page properly" do
create(:pro_membership, user: user)
sign_in user
get "/dashboard/pro"
expect(response.body).to include("pro")
end
end
context "when user has pro permission and is an org admin" do
it "shows page properly" do
org = create :organization

View file

@ -1,173 +0,0 @@
require "rails_helper"
RSpec.describe "Pro Memberships", type: :request do
describe "GET /pro" do
it "returns Pro landing page" do
get pro_membership_path
expect(response.body).to include("Like a Pro")
end
end
describe "POST /pro" do
let(:user) { create(:user) }
context "when the user is not logged in" do
it "redirects to the sign up page" do
post pro_membership_path
expect(response).to redirect_to(sign_up_path)
end
end
context "when the user is logged in and already has a pro memberships" do
before do
sign_in user
end
it "does not authorize creation if it has an active membership" do
create(:pro_membership, user: user)
expect do
post pro_membership_path
end.to raise_error(Pundit::NotAuthorizedError)
end
it "does not authorize creation if it has an expired membership" do
Timecop.freeze(Time.current) do
membership = create(:pro_membership, user: user)
membership.expire!
expect do
post pro_membership_path
end.to raise_error(Pundit::NotAuthorizedError)
end
end
it "does not authorize creation if the user as a pro role" do
user.add_role(:pro)
expect do
post pro_membership_path
end.to raise_error(Pundit::NotAuthorizedError)
end
end
context "when the user is logged in without a pro membership and enough credits" do
before do
sign_in user
create_list(:credit, ProMembership::MONTHLY_COST, user: user)
end
it "creates an active pro membership" do
expect do
post pro_membership_path
end.to change(ProMembership, :count).by(1)
expect(user.reload.pro_membership.active?).to be(true)
end
it "buys the pro membership with the correct amount of credits" do
expect do
post pro_membership_path
end.to change(user.credits.spent, :count).by(ProMembership::MONTHLY_COST)
end
it "enqueues a job to bust the user's cache" do
ActiveJob::Base.queue_adapter.enqueued_jobs.clear # make sure it hasn't been previously queued
sidekiq_assert_enqueued_with(job: Users::BustCacheWorker, args: [user.id]) do
post pro_membership_path
end
end
it "enqueues a job to bust the user's articles caches" do
sidekiq_assert_enqueued_with(
job: Users::ResaveArticlesWorker,
args: [user.id],
queue: "medium_priority",
) do
post pro_membership_path
end
end
it "adds the user to the pro members channel" do
create(:chat_channel, channel_type: "invite_only", slug: "pro-members")
post pro_membership_path
expect(user.reload.chat_channels.exists?(slug: "pro-members")).to be(true)
end
it "redirects to the pro membership settings page with a notice" do
post pro_membership_path
expect(response).to redirect_to(user_settings_path("pro-membership"))
expect(flash[:settings_notice]).to eq("You are now a Pro!")
end
end
context "when the user is logged in without a pro membership and not enough credits" do
before do
sign_in user
end
it "does not create an active pro membership" do
expect do
post pro_membership_path
end.to change(ProMembership, :count).by(0)
end
it "does not subtract credits" do
expect do
post pro_membership_path
end.to change(user.credits.spent, :count).by(0)
end
it "redirects to the pro membership settings page with an error message" do
post pro_membership_path
expect(response).to redirect_to(user_settings_path("pro-membership"))
expect(flash[:error]).to eq("You don't have enough credits!")
end
end
end
describe "PUT /pro" do
let(:user) { create(:user) }
context "when the user is not logged in" do
it "redirects to the sign up page" do
put pro_membership_path
expect(response).to redirect_to(sign_up_path)
end
end
context "when the user is logged in without a pro membership" do
before do
sign_in user
end
it "does not authorize the operation" do
expect do
put pro_membership_path
end.to raise_error(Pundit::NotAuthorizedError)
end
end
context "when the user is logged in with a pro membership" do
before do
sign_in user
end
it "works correctly" do
create(:pro_membership, user: user)
put pro_membership_path, params: { pro_membership: { auto_recharge: true } }
expect(flash[:settings_notice]).to eq("Your membership has been updated!")
expect(response).to redirect_to(user_settings_path("pro-membership"))
end
it "activates auto recharge" do
pro_membership = create(:pro_membership, user: user)
put pro_membership_path, params: { pro_membership: { auto_recharge: true } }
expect(pro_membership.reload.auto_recharge).to be(true)
end
it "deactivates auto recharge" do
pro_membership = create(:pro_membership, user: user, auto_recharge: true)
put pro_membership_path, params: { pro_membership: { auto_recharge: false } }
expect(pro_membership.reload.auto_recharge).to be(false)
end
end
end
end

View file

@ -1,257 +0,0 @@
require "rails_helper"
RSpec.describe ProMemberships::Biller, type: :service do
def format_date(datetime)
# PostgreSQL DATE(..) function uses UTC.
datetime.utc.to_date.iso8601
end
context "when there are expiring memberships with enough credits" do
let(:pro_membership) { create(:pro_membership) }
let(:user) { pro_membership.user }
before do
create_list(:credit, ProMembership::MONTHLY_COST, user: user)
end
it "renews the membership" do
Timecop.travel(format_date(pro_membership.expires_at)) do
described_class.call
pro_membership.reload
expect(pro_membership.expires_at.to_i).to eq(1.month.from_now.to_i)
expect(pro_membership.status).to eq("active")
end
end
it "subtracts the correct amount of credits" do
Timecop.travel(format_date(pro_membership.expires_at)) do
expect do
described_class.call
end.to change(user.credits.spent, :size).by(ProMembership::MONTHLY_COST)
end
end
it "adds the user back to the pro members chat channel" do
create(:chat_channel, slug: "pro-members", channel_type: "invite_only")
Timecop.travel(format_date(pro_membership.expires_at)) do
described_class.call
expect(user.reload.chat_channels.exists?(slug: "pro-members")).to be(true)
end
end
it "does not fail if the user is already in the pro members chat channel" do
cc = create(:chat_channel, slug: "pro-members", channel_type: "invite_only")
cc.add_users(user)
allow(Rails.logger).to receive(:error)
Timecop.travel(format_date(pro_membership.expires_at)) do
described_class.call
expect(Rails.logger).not_to have_received(:error)
expect(user.reload.chat_channels.exists?(slug: "pro-members")).to be(true)
end
end
it "enqueues a job to bust the users caches" do
ActiveJob::Base.queue_adapter.enqueued_jobs.clear # make sure it hasn't been previously queued
Timecop.travel(format_date(pro_membership.expires_at)) do
sidekiq_assert_enqueued_with(job: Users::BustCacheWorker, args: [user.id]) do
described_class.call
end
end
end
it "enqueues a job to bust the users articles caches" do
Timecop.travel(format_date(pro_membership.expires_at)) do
sidekiq_assert_enqueued_with(
job: Users::ResaveArticlesWorker,
args: [user.id],
queue: "medium_priority",
) do
described_class.call
end
end
end
context "when an error occurs" do
it "does not renew the membership" do
Timecop.travel(format_date(pro_membership.expires_at)) do
pro_membership.expire!
allow(Credits::Buyer).to receive(:call).and_raise(StandardError)
described_class.call
expect(pro_membership.expires_at.to_i).to be(Time.current.to_i)
expect(pro_membership.status).to eq("expired")
end
end
it "does not subtract credits" do
Timecop.travel(format_date(pro_membership.expires_at)) do
allow(Credits::Buyer).to receive(:call).and_raise(StandardError)
expect do
described_class.call
end.to change(user.credits.spent, :size).by(0)
end
end
it "notifies the admins about the error" do
Timecop.travel(format_date(pro_membership.expires_at)) do
allow(Credits::Buyer).to receive(:call).and_raise(StandardError)
sidekiq_assert_enqueued_with(job: Slack::Messengers::Worker) do
described_class.call
end
end
end
end
end
context "when there are expiring memberships with insufficient credits" do
let(:pro_membership) { create(:pro_membership) }
let(:user) { pro_membership.user }
it "expires the membership" do
Timecop.travel(format_date(pro_membership.expires_at)) do
described_class.call
pro_membership.reload
expect(pro_membership.expired?).to be(true)
expect(pro_membership.status).to eq("expired")
end
end
it "removes the user from the pro members chat channel" do
cc = create(:chat_channel, slug: "pro-members", channel_type: "invite_only")
cc.add_users(user)
Timecop.travel(format_date(pro_membership.expires_at)) do
described_class.call
expect(user.reload.chat_channels.exists?(slug: "pro-members")).to be(false)
end
end
it "notifies the admins about the expiration" do
Timecop.travel(format_date(pro_membership.expires_at)) do
sidekiq_assert_enqueued_with(job: Slack::Messengers::Worker) do
described_class.call
end
end
end
it "enqueues a job to bust the users caches" do
ActiveJob::Base.queue_adapter.enqueued_jobs.clear # make sure it hasn't been previously queued
Timecop.travel(format_date(pro_membership.expires_at)) do
sidekiq_assert_enqueued_with(job: Users::BustCacheWorker, args: [user.id]) do
described_class.call
end
end
end
it "enqueues a job to bust the users articles caches" do
Timecop.travel(format_date(pro_membership.expires_at)) do
sidekiq_assert_enqueued_with(
job: Users::ResaveArticlesWorker,
args: [user.id],
queue: "medium_priority",
) do
described_class.call
end
end
end
end
context "when there are expiring memberships with insufficient credits and auto recharge" do
let(:pro_membership) { create(:pro_membership, auto_recharge: true) }
let(:user) { pro_membership.user }
context "when the user has an associated customer" do
before do
StripeMock.start
customer = Payments::Customer.create
user.update_columns(stripe_id_code: customer.id)
end
after do
StripeMock.stop
end
it "charges the customer" do
customer = Payments::Customer.get(user.stripe_id_code)
allow(Payments::Customer).to receive(:charge)
Timecop.travel(format_date(pro_membership.expires_at)) do
described_class.call
end
expect(Payments::Customer).to have_received(:charge).with(
customer: customer,
amount: ProMembership::MONTHLY_COST_USD,
description: "Purchase of 5 credits.",
)
end
it "adds the correct amount of credits" do
Timecop.travel(format_date(pro_membership.expires_at)) do
# we cannot use "expect.to change" because of how activerecord-import works
old_num_credits = user.credits.size
described_class.call
expect(user.reload.credits.size).to be(old_num_credits + ProMembership::MONTHLY_COST)
end
end
it "renews the membership" do
Timecop.travel(format_date(pro_membership.expires_at)) do
described_class.call
pro_membership.reload
expect(pro_membership.expires_at.to_i).to eq(1.month.from_now.to_i)
expect(pro_membership.status).to eq("active")
end
end
it "spends the correct amount of credits" do
Timecop.travel(format_date(pro_membership.expires_at)) do
# we cannot use "expect.to change" because of how activerecord-import works
old_num_credits = user.reload.credits.spent.size
described_class.call
expect(user.reload.credits.spent.size).to eq(old_num_credits + ProMembership::MONTHLY_COST)
end
end
it "enqueues a job to bust the users caches" do
ActiveJob::Base.queue_adapter.enqueued_jobs.clear # make sure it hasn't been previously queued
Timecop.travel(format_date(pro_membership.expires_at)) do
sidekiq_assert_enqueued_with(job: Users::BustCacheWorker, args: [user.id]) do
described_class.call
end
end
end
it "enqueues a job to bust the users articles caches" do
Timecop.travel(format_date(pro_membership.expires_at)) do
sidekiq_assert_enqueued_with(
job: Users::ResaveArticlesWorker,
args: [user.id],
queue: "medium_priority",
) do
described_class.call
end
end
end
end
context "when the user has no associated customer" do
it "notifies the admins about the problem" do
allow(user).to receive(:stripe_id_code).and_return(nil)
Timecop.travel(format_date(pro_membership.expires_at)) do
sidekiq_assert_enqueued_with(job: Slack::Messengers::Worker) do
described_class.call
end
end
end
it "does not change the number of credits" do
Timecop.travel(format_date(pro_membership.expires_at)) do
expect do
described_class.call
end.to change(user.credits, :size).by(0)
end
end
end
end
end

View file

@ -1,66 +0,0 @@
require "rails_helper"
RSpec.describe ProMemberships::ExpirationNotifier, type: :service do
context "when there are expiring memberships with enough credits" do
let(:pro_membership) { create(:pro_membership) }
let(:user) { pro_membership.user }
it "does not deliver an email to the user" do
create_list(:credit, ProMembership::MONTHLY_COST, user: user)
Timecop.travel(pro_membership.expires_at - 1.week) do
assert_emails 0 do
described_class.call(1.week.from_now)
end
end
end
end
context "when there are expiring memberships with insufficient credits" do
let(:pro_membership) { create(:pro_membership) }
it "delivers an email to the user" do
Timecop.travel(pro_membership.expires_at - 1.week) do
assert_emails 1 do
described_class.call(1.week.from_now)
end
end
end
it "sets the expiration notification datetime" do
Timecop.freeze(pro_membership.expires_at - 1.week) do
described_class.call(1.week.from_now)
expect(pro_membership.reload.expiration_notification_at.to_i).to eq(Time.current.to_i)
end
end
it "increments the notifications count" do
Timecop.travel(pro_membership.expires_at - 1.week) do
expect(pro_membership.expiration_notifications_count).to eq(0)
described_class.call(1.week.from_now)
expect(pro_membership.reload.expiration_notifications_count).to eq(1)
end
end
it "enqueus a slack bot ping job" do
Timecop.travel(pro_membership.expires_at - 1.week) do
sidekiq_assert_enqueued_with(job: Slack::Messengers::Worker) do
described_class.call(1.week.from_now)
end
end
end
end
context "when there are expiring memberships with insufficient credits and auto recharge" do
let(:pro_membership) { create(:pro_membership) }
let(:user) { pro_membership.user }
it "does not deliver an email to the user" do
pro_membership.update_columns(auto_recharge: true)
Timecop.travel(pro_membership.expires_at - 1.week) do
assert_emails 0 do
described_class.call(1.week.from_now)
end
end
end
end
end

View file

@ -1,20 +0,0 @@
require "rails_helper"
RSpec.describe "Creates a Pro Membership", type: :system do
let(:user) { create(:user) }
context "when signed in as a regular user with enough credits" do
before do
create_list(:credit, 5, user: user)
sign_in user
end
it "makes the user become a pro" do
visit "/settings/pro-membership"
label = "Become a Pro member"
click_on label
expect(page).to have_content("You are now a Pro!")
end
end
end

View file

@ -1,69 +0,0 @@
require "rails_helper"
RSpec.describe "Visits Pro Memberships page", type: :system do
let(:user) { create(:user) }
context "when not signed in" do
it "does not say you are a member" do
visit "/pro"
expect(page).not_to have_content("View your Pro Membership")
end
it "does not ask to become a pro member" do
visit "/pro"
expect(page).not_to have_content("Become a Pro member")
end
end
context "when signed in as a regular user" do
before do
sign_in user
end
it "does not say you are a member" do
visit "/pro"
expect(page).not_to have_content("View your Pro Membership")
end
it "asks to become a pro member" do
visit "/pro"
expect(page).to have_content("Become a Pro member")
end
end
context "when signed in as as user with a pro role" do
before do
user.add_role(:pro)
sign_in user
end
it "says you are a member" do
visit "/pro"
expect(page).to have_content("View your Pro Membership")
end
it "does not ask to become a pro member" do
visit "/pro"
expect(page).not_to have_content("Become a Pro member")
end
end
context "when signed in as as user with a pro membership" do
before do
create(:pro_membership, user: user, expires_at: 1.month.from_now)
sign_in user
end
it "says you are a member" do
visit "/pro"
expect(page).to have_content("View your Pro Membership")
end
it "does not ask to become a pro member" do
visit "/pro"
expect(page).not_to have_content("Become a Pro member")
end
end
end

View file

@ -1,90 +0,0 @@
require "rails_helper"
RSpec.describe "Visits Pro Membership settings page", type: :system do
let(:user) { create(:user) }
let(:cost) { ProMembership::MONTHLY_COST }
context "when signed in as a regular user" do
before do
sign_in user
end
it "contains the correct info if you have no credits" do
visit "/settings/pro-membership"
expect(page).to have_content("Pro Membership is #{cost} credits per month")
expect(page).to have_content("You currently have no credits, you need #{cost} to become a Pro member")
expect(page).to have_link("Purchase credits", href: "/credits/purchase")
expect(page).to have_link("Pro Membership page", href: "/pro")
end
it "contains the correct info if you have some credits but not enough" do
create(:credit, user: user)
visit "/settings/pro-membership"
expect(page).to have_content("Pro Membership is #{cost} credits per month")
expect(page).to have_content(
"You currently have #{cost - 1} credits, you need #{cost} to become a Pro member",
)
expect(page).to have_link("Purchase credits", href: "/credits/purchase")
expect(page).to have_link("Pro Membership page", href: "/pro")
end
it "contains the correct info if you have enough credits" do
create_list(:credit, cost, user: user)
visit "/settings/pro-membership"
expect(page).to have_content("Pro Membership is #{cost} credits per month")
expect(page).to have_content("You currently have #{user.credits.unspent.size} available credits")
expect(page).not_to have_link("Purchase credits", href: "/credits/purchase")
expect(page).to have_selector("input[type=submit][value='Become a Pro member']")
expect(page).to have_link("Pro Membership page", href: "/pro")
end
end
context "when signed in as as user with a pro role" do
before do
user.add_role(:pro)
sign_in user
end
it "shows the status of the membership", js: true, percy: true do
visit "/settings/pro-membership"
Percy.snapshot(page, name: "Settings: /settings/pro-membership renders for pro role")
expect(page).to have_content("Status")
expect(page).to have_content("Expiration date")
expect(page).to have_content("Never")
expect(page).to have_link("Pro Membership page", href: "/pro")
end
end
context "when signed in as as user with a pro membership" do
before do
create(:pro_membership, user: user, expires_at: 1.month.from_now)
sign_in user
end
it "shows the status of the membership", js: true, percy: true do
visit "/settings/pro-membership"
Percy.snapshot(page, name: "Settings: /settings/pro-membership renders for pro membership")
expect(page).to have_content("Status")
expect(page).to have_content("Expiration date")
expect(page).to have_content(user.pro_membership.expires_at.to_date.to_s(:long))
expect(page).to have_content("Top up from credit card?")
expect(page).to have_selector("input[type=submit]")
expect(page).to have_link("Pro Membership page", href: "/pro")
end
it "updates the auto recharge" do
visit "/settings/pro-membership"
check "pro_membership[auto_recharge]"
click_on "SUBMIT"
expect(page).to have_content("Your membership has been updated!")
end
end
end