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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/controllers/providers_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ def safe_params
"twitterHandle",
"memberType",
"nonProfitStatus",
"annualRevenue",
"salesforceId",
"fromSalesforce",
"technicalContact",
Expand Down Expand Up @@ -424,6 +425,7 @@ def safe_params
"groupEmail" => :group_email,
"systemEmail" => :system_email,
"nonProfitStatus" => :non_profit_status,
"annualRevenue" => :annual_revenue,
"salesforceId" => :salesforce_id,
"fromSalesforce" => :from_salesforce,
"globusUuid" => :globus_uuid,
Expand Down
18 changes: 18 additions & 0 deletions app/models/provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Provider < ApplicationRecord
secondary_billing_contact
voting_contact
billing_information
annual_revenue
salesforce_id
password
updated
Expand Down Expand Up @@ -70,6 +71,14 @@ class Provider < ApplicationRecord
attr_readonly :symbol
attr_reader :from_salesforce

ANNUAL_REVENUE_OPTIONS = [
"0-500.000€",
"500.001€-2.000.000€",
"2.000.001€-50.000.000€",
"50.000.001€-1.000.000.000€",
">1.000.000.000€",
].freeze

delegate :salesforce_id, to: :consortium, prefix: true, allow_nil: true

validates_presence_of :symbol, :name, :display_name, :system_email
Expand Down Expand Up @@ -124,6 +133,13 @@ class Provider < ApplicationRecord
in: %w[non-profit for-profit],
message:
"non-profit status '%s' is not included in the list"
validates_inclusion_of :annual_revenue,
in: ANNUAL_REVENUE_OPTIONS,
allow_blank: true,
message:
"annual revenue '%s' is not a valid value"
validates_presence_of :annual_revenue,
if: -> { non_profit_status == "for-profit" }
validates_inclusion_of :focus_area,
in: %w[
naturalSciences
Expand Down Expand Up @@ -883,6 +899,7 @@ def to_jsonapi
"country_code" => country_code,
"logo_url" => logo_url,
"non_profit_status" => non_profit_status,
"annual_revenue" => annual_revenue,
"focus_area" => focus_area,
"organization_type" => organization_type,
"member_type" => member_type_label,
Expand Down Expand Up @@ -1001,6 +1018,7 @@ def set_defaults
self.billing_information = {} if billing_information.blank?
self.consortium_id = nil unless member_type == "consortium_organization"
self.non_profit_status = "non-profit" if non_profit_status.blank?
self.annual_revenue = nil if non_profit_status == "non-profit"
self.doi_estimate = doi_estimate.to_i

# custom filename for attachment as data URLs don't support filenames
Expand Down
12 changes: 12 additions & 0 deletions app/serializers/provider_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,18 @@ class ProviderSerializer
},
&:globus_uuid

attribute :annual_revenue,
if:
Proc.new { |object, params|
params[:current_ability] &&
params[:current_ability].can?(
:read_billing_information,
object,
) ==
true
},
&:annual_revenue

attribute :system_email,
if:
Proc.new { |object, params|
Expand Down
7 changes: 7 additions & 0 deletions db/migrate/20261015120000_add_annual_revenue_to_allocator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# frozen_string_literal: true

class AddAnnualRevenueToAllocator < ActiveRecord::Migration[7.2]
def change
add_column :allocator, :annual_revenue, :string, limit: 191
end
end
4 changes: 3 additions & 1 deletion db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
#
# It's strongly recommended that you check this file into your version control system.

ActiveRecord::Schema[8.1].define(version: 2026_10_01_103900) do
ActiveRecord::Schema[8.1].define(version: 2026_10_15_120000) do

create_table "active_storage_attachments", charset: "utf8mb4", collation: "utf8mb4_0900_ai_ci", force: :cascade do |t|
t.bigint "blob_id", null: false
t.datetime "created_at", null: false
Expand Down Expand Up @@ -65,6 +66,7 @@
t.datetime "logo_updated_at"
t.string "name", null: false
t.string "non_profit_status", limit: 191
t.string "annual_revenue", limit: 191
t.string "organization_type", limit: 191
t.string "password"
t.string "region"
Expand Down
63 changes: 63 additions & 0 deletions lib/tasks/provider.rake
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,67 @@ namespace :provider do
provider.send_provider_export_message(provider.to_jsonapi.merge(slack_output: true))
puts "Exported metadata for provider #{provider.symbol}."
end

# One-off: backfill annual_revenue from a CSV of For-profit Organizations exported from a Salesforce report before enabling
# Fabrica -> Salesforce sync for this field.
#
# Expected CSV columns:
# Organization ID, Annual Revenue (For-Profit)
#
# Usage:
# DRY_RUN=true CSV_FILE=/path/to/export.csv bundle exec rake provider:backfill_annual_revenue
# DRY_RUN=false CSV_FILE=/path/to/export.csv bundle exec rake provider:backfill_annual_revenue
desc "Backfill annual_revenue from Salesforce CSV (one-off)"
task backfill_annual_revenue: :environment do
require "csv"

csv_file = ENV["CSV_FILE"].presence || ENV["FILE"].presence
if csv_file.blank?
puts "ENV['CSV_FILE'] is required (path to Salesforce CSV export)."
exit 1
end
unless File.exist?(csv_file)
puts "CSV file not found: #{csv_file}"
exit 1
end

dry_run = ActiveModel::Type::Boolean.new.cast(ENV.fetch("DRY_RUN", "true"))
puts dry_run ? "Dry run (set DRY_RUN=false to apply)." : "Applying updates."

stats = { rows: 0, skipped_blank: 0, not_found: 0, updated: 0 }

CSV.foreach(csv_file, headers: true, encoding: "UTF-8") do |row|
stats[:rows] += 1

salesforce_id = row["Organization ID"].to_s.strip
annual_revenue = row["Annual Revenue (For-Profit)"].to_s.strip

if annual_revenue.blank?
stats[:skipped_blank] += 1
next
end

provider = Provider.where(salesforce_id: salesforce_id).first
if provider.nil?
stats[:not_found] += 1
puts "Skip #{salesforce_id}: provider not found"
next
end

if dry_run
stats[:updated] += 1
puts "Would update #{provider.symbol} (#{salesforce_id}): #{annual_revenue.inspect}"
next
end

# Bypass validations/callbacks so raw SF values are stored and SF is not re-exported.
provider.update_column(:annual_revenue, annual_revenue)
stats[:updated] += 1
puts "Updated #{provider.symbol} (#{salesforce_id}): #{annual_revenue.inspect}"
end

puts "\nSummary:"
stats.each { |key, value| puts " #{key}: #{value}" }
puts dry_run ? "\nDry run complete. Re-run with DRY_RUN=false to apply." : "\nBackfill complete."
end
end
2 changes: 1 addition & 1 deletion spec/models/activity_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
let(:provider) { create(:provider) }

it "activity exists" do
provider.update(non_profit_status: "for-profit")
provider.update(non_profit_status: "for-profit", annual_revenue: "0-500.000€")
Comment thread
jrhoads marked this conversation as resolved.

expect(provider.activities.length).to eq(2)
activity = provider.activities.last
Expand Down
45 changes: 45 additions & 0 deletions spec/models/provider_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@

it "for-profit" do
subject.non_profit_status = "for-profit"
subject.annual_revenue = "0-500.000€"
expect(subject.save).to be true
expect(subject.errors.details).to be_empty
end
Expand All @@ -92,6 +93,50 @@
end
end

describe "annual revenue" do
subject { build(:provider) }

it "valid option for for-profit" do
subject.non_profit_status = "for-profit"
subject.annual_revenue = "500.001€-2.000.000€"
expect(subject.save).to be true
expect(subject.errors.details).to be_empty
expect(subject.annual_revenue).to eq("500.001€-2.000.000€")
end

it "required for for-profit" do
subject.non_profit_status = "for-profit"
subject.annual_revenue = nil
expect(subject.save).to be false
expect(subject.errors.details).to eq(
annual_revenue: [{ error: :blank }],
)
end

it "not required for non-profit" do
subject.non_profit_status = "non-profit"
subject.annual_revenue = nil
expect(subject.save).to be true
expect(subject.errors.details).to be_empty
end

it "cleared when non-profit" do
subject.non_profit_status = "non-profit"
subject.annual_revenue = "0-500.000€"
expect(subject.save).to be true
expect(subject.annual_revenue).to be_nil
end

it "invalid option" do
subject.non_profit_status = "for-profit"
subject.annual_revenue = "invalid-option"
expect(subject.save).to be false
expect(subject.errors.details).to eq(
annual_revenue: [{ error: :inclusion, value: "invalid-option" }],
)
end
end

describe "logo" do
subject { build(:provider) }

Expand Down
83 changes: 83 additions & 0 deletions spec/requests/providers_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,24 @@
provider.billing_information,
)
expect(json["data"]["attributes"]["rorId"]).to eq(provider.ror_id)
expect(json["data"]["attributes"].key?("annualRevenue")).to be true
end
end

context "annual revenue is hidden without billing permission" do
let(:provider) do
create(
:provider,
non_profit_status: "for-profit",
annual_revenue: "0-500.000€",
)
end

it "omits annual revenue for anonymous requests" do
get "/providers/#{provider.symbol.downcase}"

expect(last_response.status).to eq(200)
expect(json["data"]["attributes"]["annualRevenue"]).to be_nil
end
end

Expand Down Expand Up @@ -364,6 +382,7 @@
)
expect(json["data"].dig("attributes", "billingInformation")).to eq(nil)
expect(json["data"].dig("attributes", "twitterHandle")).to eq(nil)
expect(json["data"].dig("attributes", "annualRevenue")).to eq(nil)
end
end

Expand Down Expand Up @@ -566,6 +585,39 @@
end
end

context "request is valid with annual revenue for for-profit" do
let(:params) do
{
"data" => {
"type" => "providers",
"attributes" => {
"symbol" => "FP",
"name" => "For Profit Org",
"displayName" => "For Profit Org",
"region" => "EMEA",
"systemEmail" => "doe@joe.joe",
"website" => "https://www.example.org",
"country" => "GB",
"nonProfitStatus" => "for-profit",
"annualRevenue" => "0-500.000€",
},
},
}
end

it "creates a provider" do
post "/providers", params, admin_headers

expect(last_response.status).to eq(200)
expect(json.dig("data", "attributes", "nonProfitStatus")).to eq(
"for-profit",
)
expect(json.dig("data", "attributes", "annualRevenue")).to eq(
"0-500.000€",
)
end
end

context "request is valid with contact information" do
let(:params) do
{
Expand Down Expand Up @@ -946,6 +998,37 @@
end
end

context "updates annual revenue for for-profit" do
let(:provider) do
create(
:provider,
consortium: consortium,
role_name: "ROLE_CONSORTIUM_ORGANIZATION",
non_profit_status: "for-profit",
annual_revenue: "0-500.000€",
)
end
let(:params) do
{
"data" => {
"type" => "providers",
"attributes" => {
"annualRevenue" => "500.001€-2.000.000€",
},
},
}
end

it "updates the record" do
put "/providers/#{provider.symbol}", params, headers

expect(last_response.status).to eq(200)
expect(json.dig("data", "attributes", "annualRevenue")).to eq(
"500.001€-2.000.000€",
)
end
end

context "when updating as consortium_organization" do
let(:consortium_organization_credentials) do
User.encode_auth_param(username: provider.symbol, password: "12345")
Expand Down
Loading