[deploy] Remove Unused Pro Memberships (#7894)
This commit is contained in:
parent
f65d61c8db
commit
1c461f8891
53 changed files with 25 additions and 1590 deletions
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -29,7 +29,6 @@ class DashboardManifest
|
|||
html_variant_trials
|
||||
html_variant_successes
|
||||
sponsorships
|
||||
pro_memberships
|
||||
classified_listing_categories
|
||||
].freeze
|
||||
# DASHBOARDS = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -99,10 +99,6 @@ class Organization < ApplicationRecord
|
|||
credits.unspent.size >= num_credits_needed
|
||||
end
|
||||
|
||||
def pro?
|
||||
false
|
||||
end
|
||||
|
||||
def banned
|
||||
false
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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" %>"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 %>
|
||||
|
|
|
|||
|
|
@ -1,3 +0,0 @@
|
|||
<a href="<%= pro_membership_path %>">
|
||||
Pro Membership
|
||||
</a>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 %>
|
||||
|
|
@ -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>
|
||||
|
|
@ -1 +0,0 @@
|
|||
<% # placeholder for future "pro indicator" %>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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 %>"}'> </button>
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
Loading…
Add table
Reference in a new issue