diff --git a/app/controllers/stash_engine/resources_controller.rb b/app/controllers/stash_engine/resources_controller.rb
index 4366fa68e5..09d086ffa3 100644
--- a/app/controllers/stash_engine/resources_controller.rb
+++ b/app/controllers/stash_engine/resources_controller.rb
@@ -153,6 +153,13 @@ def display_readme
end
def dpc_status
+ user_payer_aff = StashEngine::Tenant.find_by_ror_id(@resource.identifier&.submitter_affiliation&.ror_id)&.connect_list
+ aff_tenant = if @resource.tenant_id.in?(user_payer_aff.ids)
+ user_payer_aff.find_by(id: @resource.tenant_id)
+ else
+ user_payer_aff.first
+ end
+
@resource.check_add_readme_file
@resource.check_add_cedar_json
dpc_checks = {
@@ -161,7 +168,7 @@ def dpc_status
institution_will_pay: @resource.identifier.institution_will_pay?,
funder_will_pay: @resource.identifier.funder_will_pay?,
user_must_pay: @resource.identifier.user_must_pay?,
- aff_tenant: StashEngine::Tenant.find_by_ror_id(@resource.identifier&.submitter_affiliation&.ror_id)&.connect_list&.first,
+ aff_tenant: aff_tenant,
allow_review: @resource.identifier.allow_review?,
automatic_ppr: @resource.identifier.automatic_ppr?,
man_decision_made: @resource.identifier.has_accepted_manuscript? || @resource.identifier.has_rejected_manuscript?
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 0f5982068d..eda95d0c3e 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -70,7 +70,7 @@ def readme_render(content)
end
def ldf_pricing_tiers_options
- [['No limit', '']] + FeeCalculator::BaseService::ESTIMATED_FILES_SIZE.map do |tier|
+ [['No limit', '']] + ESTIMATED_FILES_SIZE.map do |tier|
["#{filesize(tier[:range].max)} ($#{tier[:price]})", tier[:tier]]
end
end
diff --git a/app/javascript/react/components/MetadataEntry/Agreements/Agreements.jsx b/app/javascript/react/components/MetadataEntry/Agreements/Agreements.jsx
index cb8614b41f..9dda5292ad 100644
--- a/app/javascript/react/components/MetadataEntry/Agreements/Agreements.jsx
+++ b/app/javascript/react/components/MetadataEntry/Agreements/Agreements.jsx
@@ -104,6 +104,40 @@ export default function Agreements({
@@ -280,19 +305,9 @@ export default function Agreements({
)}
>
)}
- {preview && (
-
- {resource.accepted_agreement ? (
-
- {' '}
- The submitter has agreed to Dryad's{' '}
- terms of submission
-
- ) : (
-
{' '} Terms not yet accepted
- )}
-
- )}
+
>
);
+
+
}
diff --git a/app/models/payment_configuration.rb b/app/models/payment_configuration.rb
index 69b3020665..c1a07fee02 100644
--- a/app/models/payment_configuration.rb
+++ b/app/models/payment_configuration.rb
@@ -26,6 +26,9 @@ class PaymentConfiguration < ApplicationRecord
private
def reset_limit
- self.ldf_limit = nil unless covers_ldf
+ return if covers_ldf
+
+ self.ldf_limit = nil
+ self.yearly_ldf_limit = nil
end
end
diff --git a/app/models/sponsored_payment_log.rb b/app/models/sponsored_payment_log.rb
index 0c44c4975b..389eadbda4 100644
--- a/app/models/sponsored_payment_log.rb
+++ b/app/models/sponsored_payment_log.rb
@@ -3,6 +3,7 @@
# Table name: sponsored_payment_logs
#
# id :bigint not null, primary key
+# deleted_at :datetime
# dpc :integer
# ldf :integer
# payer_type :string(191)
@@ -10,12 +11,16 @@
# updated_at :datetime not null
# payer_id :string(191)
# resource_id :integer
+# sponsor_id :string(191)
#
# Indexes
#
+# index_sponsored_payment_logs_on_deleted_at (deleted_at)
# index_sponsored_payment_logs_on_payer_id_and_payer_type (payer_id,payer_type)
+# index_sponsored_payment_logs_on_sponsor_id (sponsor_id)
#
class SponsoredPaymentLog < ApplicationRecord
+ acts_as_paranoid
belongs_to :payer, polymorphic: true
belongs_to :resource, class_name: StashEngine::Resource.to_s
diff --git a/app/models/stash_engine/identifier.rb b/app/models/stash_engine/identifier.rb
index d5f1259aad..c7a72e9941 100644
--- a/app/models/stash_engine/identifier.rb
+++ b/app/models/stash_engine/identifier.rb
@@ -72,6 +72,7 @@ class Identifier < ApplicationRecord
belongs_to :software_license, class_name: 'StashEngine::SoftwareLicense', optional: true
has_many :curation_activities, class_name: 'StashEngine::CurationActivity', through: :resources
has_many :payments, class_name: 'ResourcePayment', through: :resources
+ has_many :sponsored_payment_logs, through: :resources
after_create :create_process_date, unless: :process_date
after_create :create_share
diff --git a/app/models/stash_engine/resource.rb b/app/models/stash_engine/resource.rb
index f7c5410320..0889dd2fd0 100644
--- a/app/models/stash_engine/resource.rb
+++ b/app/models/stash_engine/resource.rb
@@ -122,6 +122,7 @@ class Resource < ApplicationRecord # rubocop:disable Metrics/ClassLength
has_one :flag, class_name: 'StashEngine::Flag', as: :flaggable, dependent: :destroy
has_many :flags, ->(resource) { unscope(where: :resource_id).where(flaggable: [resource.journal, resource.tenant, resource.users]) }
has_one :payment, class_name: 'ResourcePayment'
+ has_one :sponsored_payment_log, dependent: :destroy
after_create :create_process_date, unless: :process_date
after_update_commit :update_salesforce_metadata, if: [:saved_change_to_user_id?, proc { |res| res.curator&.min_curator? }]
diff --git a/app/models/stash_engine/support/payment_methods.rb b/app/models/stash_engine/support/payment_methods.rb
index a7f6df8829..e993c4518b 100644
--- a/app/models/stash_engine/support/payment_methods.rb
+++ b/app/models/stash_engine/support/payment_methods.rb
@@ -13,7 +13,7 @@ module PaymentMethods
def user_must_pay?
return false if latest_resource.resource_type&.resource_type == 'collection'
return false if waiver? && old_payment_system
- return PaymentLimitsService.new(latest_resource, payer).limits_exceeded? if sponsored?
+ return PaymentLimitsService.new(latest_resource, PayersService.new(payer).payment_sponsor).limits_exceeded? if sponsored?
true
end
@@ -121,8 +121,7 @@ def institution_will_pay?
# do not remove recorded institution sponsor due to sponsorship change
return true if payment_id.present? && payment_id == tenant&.id
-
- return false unless tenant&.payment_configuration&.covers_dpc
+ return false unless PayersService.new(tenant).payment_sponsor&.payment_configuration&.covers_dpc
if tenant&.authentication&.strategy == 'author_match'
# get all unique ror_id associations for all authors
@@ -181,7 +180,10 @@ def clear_payment_for_changed_sponsor
self.payment_type = nil
self.payment_id = nil
+ self.last_invoiced_file_size = 0
save
+
+ sponsored_payment_logs.destroy_all
reload
end
end
diff --git a/app/models/stash_engine/tenant.rb b/app/models/stash_engine/tenant.rb
index 7684c405dc..5fb5e4a46c 100644
--- a/app/models/stash_engine/tenant.rb
+++ b/app/models/stash_engine/tenant.rb
@@ -49,7 +49,32 @@ class Tenant < ApplicationRecord
# return all enabled tenants sorted by name
scope :enabled, -> { where(enabled: true).order(:short_name) }
scope :partner_list, -> { enabled.where(partner_display: true) }
- scope :connect_list, -> { partner_list.joins(:payment_configuration).where(payment_configurations: { covers_dpc: true }) }
+ scope :connect_list, -> {
+ # Get all root tenant IDs that have covers_dpc: true
+ root_ids = joins(:payment_configuration)
+ .where(sponsor_id: nil, payment_configurations: { covers_dpc: true })
+ .pluck(:id)
+
+ return none if root_ids.empty?
+
+ # Use a recursive CTE to get all descendants of those roots
+ partner_list.where(<<~SQL, root_ids: root_ids)
+ stash_engine_tenants.id IN (
+ WITH RECURSIVE tenant_tree AS (
+ SELECT id, sponsor_id
+ FROM stash_engine_tenants
+ WHERE id IN (:root_ids)
+
+ UNION ALL
+
+ SELECT t.id, t.sponsor_id
+ FROM stash_engine_tenants t
+ INNER JOIN tenant_tree tt ON t.sponsor_id = tt.id
+ )
+ SELECT id FROM tenant_tree
+ )
+ SQL
+ }
scope :tiered, -> { enabled.joins(:payment_configuration).where(payment_configurations: { payment_plan: 'TIERED' }) }
scope :fees_2025, -> { enabled.joins(:payment_configuration).where(payment_configurations: { payment_plan: '2025' }) }
scope :sponsored, -> { enabled.distinct.joins(:sponsored) }
@@ -114,5 +139,10 @@ def full_url(path)
end
end
+ def payment_sponsor
+ sponsor_obj = self
+ sponsor_obj = sponsor_obj.sponsor while sponsor_obj.sponsor.present?
+ sponsor_obj
+ end
end
end
diff --git a/app/services/curation_service.rb b/app/services/curation_service.rb
index 0573a568a1..f6075e8961 100644
--- a/app/services/curation_service.rb
+++ b/app/services/curation_service.rb
@@ -149,7 +149,7 @@ def process_payment
end
def processed_sponsored_resource
- return unless @status.in?(%w[queued peer_review])
+ return unless @status.in?(%w[processing queued peer_review])
SponsoredPaymentsService.new(@resource).log_payment
end
diff --git a/app/services/fee_calculator/base_service.rb b/app/services/fee_calculator/base_service.rb
index 07ceabb388..457d3da5ee 100644
--- a/app/services/fee_calculator/base_service.rb
+++ b/app/services/fee_calculator/base_service.rb
@@ -2,45 +2,13 @@ module FeeCalculator
class BaseService
attr_reader :options, :resource
- # rubocop:disable Layout/SpaceInsideRangeLiteral, Layout/ExtraSpacing
- ESTIMATED_DATASETS = [
- { tier: 1, range: 0.. 5, price: 0 },
- { tier: 2, range: 6.. 15, price: 1_650 },
- { tier: 3, range: 16.. 25, price: 2_700 },
- { tier: 4, range: 26.. 50, price: 5_350 },
- { tier: 5, range: 51.. 75, price: 7_950 },
- { tier: 6, range: 76..100, price: 10_500 },
- { tier: 7, range: 101..150, price: 15_600 },
- { tier: 8, range: 151..200, price: 20_500 },
- { tier: 9, range: 201..250, price: 25_500 },
- { tier: 10, range: 251..300, price: 30_250 },
- { tier: 11, range: 301..350, price: 35_000 },
- { tier: 12, range: 351..400, price: 39_500 },
- { tier: 13, range: 401..450, price: 44_000 },
- { tier: 14, range: 451..500, price: 48_750 },
- { tier: 15, range: 501..550, price: 53_500 },
- { tier: 16, range: 551..600, price: 58_250 }
- ].freeze
-
- ESTIMATED_FILES_SIZE = [
- { tier: 0, range: 0.. 10_000_000_000, price: 0 },
- { tier: 1, range: 10_000_000_001.. 50_000_000_000, price: 259 },
- { tier: 2, range: 50_000_000_001.. 100_000_000_000, price: 464 },
- { tier: 3, range: 100_000_000_001.. 250_000_000_000, price: 1_123 },
- { tier: 4, range: 250_000_000_001.. 500_000_000_000, price: 2_153 },
- { tier: 5, range: 500_000_000_001..1_000_000_000_000, price: 4_347 },
- { tier: 6, range: 1_000_000_000_001..2_000_000_000_000, price: 8_809 }
- ].freeze
-
- INVOICE_FEE = 199
- # rubocop:enable Layout/SpaceInsideRangeLiteral, Layout/ExtraSpacing
-
def initialize(options = {}, resource: nil, payer_record: nil)
@sum = 0
@options = options
@sum_options = {}
@resource = resource
@payer = payer_record || (resource ? resource.identifier.payer : nil)
+ @payer = PayersService.new(@payer).payment_sponsor if @payer
@payment_plan_is_2025 = resource ? resource.identifier.payer_2025?(@payer) : false
@covers_ldf = resource ? @payer&.payment_configuration&.covers_ldf : false
@ldf_limit = resource ? @payer&.payment_configuration&.ldf_limit : nil
@@ -53,11 +21,23 @@ def call
if resource.present?
add_zero_fee(:service_tier)
add_zero_fee(:dpc_tier)
+ limits_service = PaymentLimitsService.new(resource, @payer, ldf_sponsored_amount: ldf_sponsored_amount)
+
if @covers_ldf
- if @ldf_limit.nil?
+ @sum_options[:storage_fee_label] = PRODUCT_NAME_MAPPER[:storage_fee_overage] unless @ldf_limit.nil?
+ if @ldf_limit.nil? && limits_service.payment_allowed?
+ # if no limit is hit,
+ # the user pays no storage fee
verify_max_storage_size
add_zero_fee(:storage_size)
+ elsif limits_service.amount_limits_exceeded?
+ # if the yearly amount limit is hit,
+ # the user needs to pay the full storage difference
+ add_storage_fee_difference
+ add_invoice_fee
else
+ # if the amount by adding sponsored storage fee is not exceeded
+ # user mult pay the difference between sponsored size and resource size
handle_ldf_limit
end
else
@@ -90,10 +70,33 @@ def handle_ldf_limit
add_invoice_fee
end
+ def ldf_sponsored_amount(paid_storage_size: nil)
+ paid_storage_size ||= resource.identifier.last_invoiced_file_size.to_i
+ paid_tier_price = price_by_range(storage_fee_tiers, paid_storage_size)
+
+ new_tier_price = if @ldf_limit.nil?
+ price_by_range(storage_fee_tiers, resource.total_file_size)
+ else
+ new_tier_price = price_by_range(storage_fee_tiers, resource.total_file_size)
+ tier = get_tier_by_value(storage_fee_tiers, @ldf_limit)
+ [new_tier_price, tier[:price]].min
+ end
+
+ diff = new_tier_price - paid_tier_price
+ diff = 0 if diff < 0
+ diff.to_f
+ end
+
def storage_fee_tier
get_tier_by_range(storage_fee_tiers, resource.total_file_size)
end
+ # if tier is not matched, consider first tier
+ def get_tier_by_value(tier_definition, value)
+ tier = tier_definition.find { |t| t[:tier] == value.to_i }
+ tier || tier_definition.find { |t| t[:tier] == 1 }
+ end
+
private
def verify_new_payment_system
@@ -192,12 +195,6 @@ def price_by_tier(tier_definition, value)
tier[:price].to_i
end
- # if tier is not matched, consider first tier
- def get_tier_by_value(tier_definition, value)
- tier = tier_definition.find { |t| t[:tier] == value.to_i }
- tier || tier_definition.find { |t| t[:tier] == 1 }
- end
-
def add_fee_by_range(tier_definition, value_key)
value = price_by_range(tier_definition, options[value_key])
add_fee_to_total(value_key, value)
diff --git a/app/services/fee_calculator/individual_service.rb b/app/services/fee_calculator/individual_service.rb
index 12041bfeca..e5fe075255 100644
--- a/app/services/fee_calculator/individual_service.rb
+++ b/app/services/fee_calculator/individual_service.rb
@@ -1,21 +1,5 @@
module FeeCalculator
class IndividualService < BaseService
- # rubocop:disable Layout/SpaceInsideRangeLiteral, Layout/ExtraSpacing
- INDIVIDUAL_ESTIMATED_FILES_SIZE = [
- { tier: 1, range: 0.. 5_000_000_000, price: 150 },
- { tier: 2, range: 5_000_000_001.. 10_000_000_000, price: 180 },
- { tier: 3, range: 10_000_000_001.. 50_000_000_000, price: 520 },
- { tier: 4, range: 50_000_000_001.. 100_000_000_000, price: 808 },
- { tier: 5, range: 100_000_000_001.. 250_000_000_000, price: 1_750 },
- { tier: 6, range: 250_000_000_001.. 500_000_000_000, price: 3_086 },
- { tier: 7, range: 500_000_000_001..1_000_000_000_000, price: 6_077 },
- { tier: 8, range: 1_000_000_000_001..2_000_000_000_000, price: 12_162 }
- ].freeze
-
- PPR_FEE = 50
- PPR_COUPON_ID = 'PPR_DISCOUNT_2025'.freeze
- # rubocop:enable Layout/SpaceInsideRangeLiteral, Layout/ExtraSpacing
-
def call
verify_new_payment_system
verify_max_storage_size if resource
diff --git a/app/services/fee_calculator/institution_service.rb b/app/services/fee_calculator/institution_service.rb
index 73c4b1d231..f1577ca0aa 100644
--- a/app/services/fee_calculator/institution_service.rb
+++ b/app/services/fee_calculator/institution_service.rb
@@ -1,23 +1,5 @@
module FeeCalculator
class InstitutionService < BaseService
- # rubocop:disable Layout/SpaceInsideRangeLiteral, Layout/ExtraSpacing
- NORMAL_SERVICE_FEE = [
- { tier: 1, range: 0.. 100_000_000, price: 5_000 },
- { tier: 2, range: 100_000_001.. 250_000_000, price: 10_000 },
- { tier: 3, range: 250_000_001.. 500_000_000, price: 20_000 },
- { tier: 4, range: 500_000_001.. 750_000_000, price: 30_000 },
- { tier: 5, range: 750_000_001.. 1_000_000_000, price: 40_000 },
- { tier: 6, range: 1_000_000_001..Float::INFINITY, price: 50_000 }
- ].freeze
-
- LOW_MIDDLE_SERVICE_FEE = [
- { tier: 1, range: 0.. 5_000_000, price: 1_000 },
- { tier: 2, range: 5_000_001.. 25_000_000, price: 1_500 },
- { tier: 3, range: 25_000_001.. 50_000_000, price: 2_500 },
- { tier: 4, range: 50_000_001.. 100_000_000, price: 5_000 },
- { tier: 5, range: 100_000_001..Float::INFINITY, price: 7_500 }
- ].freeze
- # rubocop:enable Layout/SpaceInsideRangeLiteral, Layout/ExtraSpacing
def service_fee_tiers
return LOW_MIDDLE_SERVICE_FEE if options[:low_middle_income_country]
diff --git a/app/services/fee_calculator/publisher_service.rb b/app/services/fee_calculator/publisher_service.rb
index 9b6ce3430d..23ce00903e 100644
--- a/app/services/fee_calculator/publisher_service.rb
+++ b/app/services/fee_calculator/publisher_service.rb
@@ -1,22 +1,8 @@
module FeeCalculator
class PublisherService < BaseService
- # rubocop:disable Layout/SpaceInsideRangeLiteral, Layout/ExtraSpacing
- SERVICE_FEE = [
- { tier: 1, range: 0.. 500_000, price: 1_000 },
- { tier: 2, range: 500_001.. 1_000_000, price: 2_500 },
- { tier: 3, range: 1_000_001.. 5_000_000, price: 5_000 },
- { tier: 4, range: 5_000_001.. 10_000_000, price: 7_500 },
- { tier: 5, range: 10_000_001.. 25_000_000, price: 10_000 },
- { tier: 6, range: 25_000_001.. 50_000_000, price: 12_500 },
- { tier: 7, range: 50_000_001.. 100_000_000, price: 15_000 },
- { tier: 8, range: 100_000_001.. 200_000_000, price: 22_500 },
- { tier: 9, range: 200_000_001.. 500_000_000, price: 30_000 },
- { tier: 10, range: 500_000_001..Float::INFINITY, price: 40_000 }
- ].freeze
- # rubocop:enable Layout/SpaceInsideRangeLiteral, Layout/ExtraSpacing
def service_fee_tiers
- SERVICE_FEE
+ PUBLISHER_SERVICE_FEE
end
end
end
diff --git a/app/services/fee_calculator/waiver_service.rb b/app/services/fee_calculator/waiver_service.rb
index dbbac93f18..8b4b65aba5 100644
--- a/app/services/fee_calculator/waiver_service.rb
+++ b/app/services/fee_calculator/waiver_service.rb
@@ -1,9 +1,6 @@
module FeeCalculator
class WaiverService < IndividualService
- DISCOUNT_STORAGE_COUPON_ID = 'FEE_WAIVER_2025'.freeze
- FREE_STORAGE_SIZE = 10_000_000_000 # 10 GB
-
private
def storage_fee_label
diff --git a/app/services/fee_calculator_service.rb b/app/services/fee_calculator_service.rb
index a920626cad..81c692e6f4 100644
--- a/app/services/fee_calculator_service.rb
+++ b/app/services/fee_calculator_service.rb
@@ -12,6 +12,22 @@ def storage_fee_tier(resource: nil)
calculator_class.constantize.new({}, resource: resource).storage_fee_tier
end
+ def ldf_sponsored_amount(resource: nil, payer: nil)
+ calculator_class.constantize.new({}, resource: resource, payer_record: payer).ldf_sponsored_amount
+ end
+
+ def sponsored_tier(payer)
+ calculator_class.constantize.new({})
+ .get_tier_by_value(
+ ESTIMATED_FILES_SIZE,
+ payer.payment_configuration.ldf_limit
+ )
+ end
+
+ def calculator_service
+ calculator_class.constantize
+ end
+
private
def calculator_class
diff --git a/app/services/payers_service.rb b/app/services/payers_service.rb
index ed0cff145a..6b1c9d33dc 100644
--- a/app/services/payers_service.rb
+++ b/app/services/payers_service.rb
@@ -6,6 +6,12 @@ def initialize(payer)
end
def is_2025_payer?
- payer.payment_configuration&.payment_plan.to_s == '2025'
+ payment_sponsor.payment_configuration&.payment_plan.to_s == '2025'
+ end
+
+ def payment_sponsor
+ payer.payment_sponsor
+ rescue StandardError
+ payer
end
end
diff --git a/app/services/payment_limits_service.rb b/app/services/payment_limits_service.rb
index c3fdeb2860..8d535b2538 100644
--- a/app/services/payment_limits_service.rb
+++ b/app/services/payment_limits_service.rb
@@ -1,40 +1,87 @@
class PaymentLimitsService
- attr_reader :resource, :payer
+ attr_reader :resource, :payer, :ldf_sponsored_amount
- def initialize(resource, payer)
+ def initialize(resource, payer, ldf_sponsored_amount: nil)
@resource = resource
@payer = payer
+ @ldf_sponsored_amount = ldf_sponsored_amount
+ set_calculator_service
end
def payment_allowed?
!limits_exceeded?
end
- def limits_exceeded?
+ def size_limits_exceeded?
+ return true if payer.nil?
+ return false unless PayersService.new(payer).is_2025_payer?
+ return true unless payer.payment_configuration&.covers_ldf
+ return false if payer.payment_configuration.ldf_limit.nil?
+
+ # true - if the new files size is over the LDF size limit
+ ldf_limit_exceeded
+ end
+
+ def amount_limits_exceeded?
return true if payer.nil?
return false unless PayersService.new(payer).is_2025_payer?
+ return true unless payer.payment_configuration&.covers_ldf
return false if payer.payment_configuration.yearly_ldf_limit.nil?
exceeds_yearly_ldf_limit?
end
+ def limits_exceeded?
+ return true if payer.nil?
+ return false unless PayersService.new(payer).is_2025_payer?
+ return true unless payer.payment_configuration&.covers_ldf
+
+ amount_limits_exceeded? || size_limits_exceeded?
+ end
+
private
def exceeds_yearly_ldf_limit?
- resource_fees = calculate_fees
- return false if resource_fees[:storage_fee].to_f.zero?
+ storage_fee = ldf_sponsored_amount || @calculator_service.ldf_sponsored_amount(resource: resource, payer: payer)
+ return false if storage_fee.zero?
+
+ exceeds_payer_yearly_limit?(storage_fee) || exceeds_sponsor_yearly_limit?(storage_fee)
+ end
+
+ def exceeds_payer_yearly_limit?(storage_fee)
+ return false if payer.payment_configuration&.yearly_ldf_limit.nil?
+
+ paid_amount = SponsoredPaymentLog.for_current_year.where(sponsor_id: payer.id)
+ .or(SponsoredPaymentLog.for_current_year.where(payer_id: payer.id))
+ .sum(:ldf)
+ paid_amount + storage_fee > payer.payment_configuration.yearly_ldf_limit.to_f
+ end
+
+ def exceeds_sponsor_yearly_limit?(storage_fee)
+ sponsor = PayersService.new(payer).payment_sponsor
+ return false if sponsor.nil?
+
+ payment_conf = sponsor.payment_configuration
+ return false if !payment_conf&.covers_ldf || payment_conf&.yearly_ldf_limit.nil?
- payer.payment_logs.for_current_year.sum(&:ldf) + resource_fees[:storage_fee].to_f > payer.payment_configuration.yearly_ldf_limit.to_f
+ paid_amount = SponsoredPaymentLog.for_current_year.where(sponsor_id: sponsor.id).sum(:ldf)
+ paid_amount + storage_fee > payment_conf.yearly_ldf_limit.to_f
end
- def calculate_fees
+ def set_calculator_service
payer_type = case payer.class.name
when 'StashEngine::Funder', 'StashEngine::Journal'
'publisher'
when 'StashEngine::Tenant'
'institution'
end
+ @calculator_service = FeeCalculatorService.new(payer_type)
+ end
+
+ def ldf_limit_exceeded
+ return false if payer.payment_configuration.ldf_limit.nil?
- FeeCalculatorService.new(payer_type).calculate({}, resource: resource, payer_record: payer)
+ tier = @calculator_service.sponsored_tier(payer)
+ tier[:range].max < resource.total_file_size.to_i
end
end
diff --git a/app/services/sponsored_payments_service.rb b/app/services/sponsored_payments_service.rb
index 7fa3e80360..109eaa5e67 100644
--- a/app/services/sponsored_payments_service.rb
+++ b/app/services/sponsored_payments_service.rb
@@ -1,48 +1,77 @@
class SponsoredPaymentsService
- attr_reader :resource, :payer
+ attr_reader :resource, :identifier, :payer
def initialize(resource)
@resource = resource
- @payer = resource.identifier.payer
+ @identifier = resource.identifier
+ @payer = @identifier.payer
end
def log_payment
# there is no payer
return if payer.nil?
- # user is flagged to pay
- return if resource.identifier.user_must_pay?
# user is not on 2025 plan
return unless PayersService.new(payer).is_2025_payer?
- # for this resource, there is an invoice or user already paid/failed with CC
- payment = resource.payment
- if payment
- # resource already has an invoice created
- return if payment.pay_with_invoice?
- # or a paid/failed CC payment
- return if !payment.pay_with_invoice? && payment.paid?
- return if !payment.pay_with_invoice? && payment.failed?
- end
+ @calculator_service = calculator_service
+ SponsoredPaymentLog.transaction do
+ paid_before = delete_larger_file_size_logs
+ amount = ldf_fees(paid_before)
+ update_identifier_files_size and return if amount.zero?
- resource_fees = calculate_fees
- SponsoredPaymentLog.create(
- resource: resource,
- payer: payer,
- ldf: resource_fees[:storage_fee]
- )
- resource.identifier.update(last_invoiced_file_size: [resource.identifier.last_invoiced_file_size.to_i, resource.total_file_size].max)
+ SponsoredPaymentLog.create(
+ resource: resource,
+ payer: payer,
+ ldf: amount,
+ sponsor_id: PayersService.new(payer).payment_sponsor&.id
+ )
+ update_identifier_files_size
+ end
end
private
- def calculate_fees
+ def update_identifier_files_size
+ identifier.update(last_invoiced_file_size: resource.total_file_size)
+ end
+
+ def ldf_fees(size = nil)
+ @calculator_service.ldf_sponsored_amount(paid_storage_size: size)
+ end
+
+ def calculator_service
+ calculator_service_class.new({}, resource: resource, payer_record: payer)
+ end
+
+ def calculator_service_class
payer_type = case payer.class.name
when 'StashEngine::Funder', 'StashEngine::Journal'
'publisher'
when 'StashEngine::Tenant'
'institution'
end
+ FeeCalculatorService.new(payer_type).calculator_service
+ end
+
+ def delete_larger_file_size_logs
+ paid_before = identifier.last_invoiced_file_size.to_i
+
+ if paid_before > resource.total_file_size
+ new_tier = calculator_service.storage_fee_tier
+ resource.previous_resources.each do |res|
+ return res.total_file_size if res.status_published?
+
+ res_tier = calculator_service_class.new({}, resource: res).storage_fee_tier
+ if new_tier[:price] < res_tier[:price]
+ res.sponsored_payment_log.destroy
+ else
+ paid_before = res.total_file_size
+ break
+ end
+ end
+ end
+ return 0 if identifier.sponsored_payment_logs.none?
- FeeCalculatorService.new(payer_type).calculate({}, resource: resource, payer_record: payer)
+ paid_before
end
end
diff --git a/app/views/stash_engine/journal_admin/_form.html.erb b/app/views/stash_engine/journal_admin/_form.html.erb
index ff9a2c350a..289c58eda9 100644
--- a/app/views/stash_engine/journal_admin/_form.html.erb
+++ b/app/views/stash_engine/journal_admin/_form.html.erb
@@ -39,7 +39,7 @@
<%= pf.label :yearly_ldf_limit, "Yearly Large Data Fee limit $" %>
- <%= pf.number_field :yearly_ldf_limit, value: @payment_configuration.yearly_ldf_limit, class: 'c-input__text' %>
+ <%= pf.number_field :yearly_ldf_limit, value: @payment_configuration.yearly_ldf_limit, class: 'c-input__text', disabled: !@payment_configuration.covers_ldf %>
<% end %>
diff --git a/app/views/stash_engine/journal_admin/edit.js.erb b/app/views/stash_engine/journal_admin/edit.js.erb
index c65b17d13b..1bc7518207 100644
--- a/app/views/stash_engine/journal_admin/edit.js.erb
+++ b/app/views/stash_engine/journal_admin/edit.js.erb
@@ -25,5 +25,10 @@ var ldf = document.getElementById('payment_configuration_attributes_covers_ldf')
ldf.addEventListener('change', () => {
var limit = document.getElementById('payment_configuration_attributes_ldf_limit')
limit.disabled = !ldf.checked
- if (!ldf.checked) limit.value = ''
+ var amount_limit = document.getElementById('payment_configuration_attributes_yearly_ldf_limit')
+ amount_limit.disabled = !ldf.checked
+ if (!ldf.checked) {
+ limit.value = ''
+ amount_limit.value = ''
+ }
})
diff --git a/app/views/stash_engine/journal_admin/new.js.erb b/app/views/stash_engine/journal_admin/new.js.erb
index 9aa461dcfb..e85ed88e02 100644
--- a/app/views/stash_engine/journal_admin/new.js.erb
+++ b/app/views/stash_engine/journal_admin/new.js.erb
@@ -25,5 +25,10 @@ var ldf = document.getElementById('payment_configuration_attributes_covers_ldf')
ldf.addEventListener('change', () => {
var limit = document.getElementById('payment_configuration_attributes_ldf_limit')
limit.disabled = !ldf.checked
- if (!ldf.checked) limit.value = ''
+ var amount_limit = document.getElementById('payment_configuration_attributes_yearly_ldf_limit')
+ amount_limit.disabled = !ldf.checked
+ if (!ldf.checked) {
+ limit.value = ''
+ amount_limit.value = ''
+ }
})
diff --git a/app/views/stash_engine/tenant_admin/_form.html.erb b/app/views/stash_engine/tenant_admin/_form.html.erb
index 3297cb5ca2..66152db0d7 100644
--- a/app/views/stash_engine/tenant_admin/_form.html.erb
+++ b/app/views/stash_engine/tenant_admin/_form.html.erb
@@ -36,45 +36,51 @@
This setting controls whether <%= @tenant.short_name.presence || 'the institution'%>, if active , is displayed on the public partner list.
<%= form.check_box :partner_display, {checked: @tenant.partner_display}, 1, 0 %> <%= form.label :partner_display, "Partner is displayed", class: 'c-input__label' %>
-<%= fields_for :payment_configuration_attributes do |pf| %>
- <%= pf.hidden_field :id, value: @payment_configuration.id %>
-
Membership
-
- Payment plan type:
- <% ([ ['', 'None'] ] + (StashEngine::Tenant::PAYMENT_PLANS.map {|p| [p, p.capitalize]})).each do |plan| %>
- <%= pf.radio_button :payment_plan, plan.first, checked: plan.first == @payment_configuration.payment_plan.to_s %>
- "><%= plan[1] %>
-
- <% end %>
-
-
DPC
-
This setting controls whether <%= @tenant.short_name.presence || 'the institution'%> covers the cost of Dryad for its users. Partners must be active , displayed , and cover the DPC for users to be invited to connect to them.
-
- <%= pf.check_box :covers_dpc, {checked: @payment_configuration.covers_dpc}, 1, 0 %>
- <%= pf.label :covers_dpc, "Partner covers the DPC", class: 'c-input__label' %>
-
-
This setting controls whether <%= @tenant.short_name.presence || 'the institution'%> covers Large Data Fees for its users.
-
- <%= pf.check_box :covers_ldf, {checked: @payment_configuration.covers_ldf}, 1, 0 %>
- <%= pf.label :covers_ldf, "Partner covers Large Data Fees", class: 'c-input__label' %>
-
-
- <%= pf.label :ldf_limit, "Covered Large Data Fee limit (GB)" %>
- <%= pf.select :ldf_limit, ldf_pricing_tiers_options, { selected: @payment_configuration.ldf_limit }, class: 'c-input__select', disabled: !@payment_configuration.covers_ldf %>
-
-
- <%= pf.label :yearly_ldf_limit, "Yearly Large Data Fee limit $" %>
- <%= pf.number_field :yearly_ldf_limit, value: @payment_configuration.yearly_ldf_limit, class: 'c-input__text' %>
-
-<% end %>
-
Sponsor
Are the institution's fees sponsored by another institution?
<%
- options = StashEngine::Tenant.enabled.all.collect { |o| [ o.short_name, o.id ] }
+ options = StashEngine::Tenant.enabled.all.collect { |o| [ o.short_name, o.id ] }
options.unshift(['None', ''])
%>
<%= label_tag 'sponsor_id', 'Choose a sponsoring institution' %>
- <%= select_tag('sponsor_id', options_for_select(options, @tenant.sponsor_id), class: 'c-input__select') %>
+ <%= select_tag('sponsor_id', options_for_select(options, @tenant.sponsor_id), class: 'c-input__select') %>
+
+
+
style="padding: 10px">
+ Payment needs to be configured at the sponsor level.
+
+
>
+ <%= fields_for :payment_configuration_attributes do |pf| %>
+
+ <%= pf.hidden_field :id, value: @payment_configuration.id %>
+
Membership
+
+ Payment plan type:
+ <% ([ ['', 'None'] ] + (StashEngine::Tenant::PAYMENT_PLANS.map {|p| [p, p.capitalize]})).each do |plan| %>
+ <%= pf.radio_button :payment_plan, plan.first, checked: plan.first == @payment_configuration.payment_plan.to_s %>
+ "><%= plan[1] %>
+
+ <% end %>
+
+
DPC
+
This setting controls whether <%= @tenant.short_name.presence || 'the institution'%> covers the cost of Dryad for its users. Partners must be active , displayed , and cover the DPC for users to be invited to connect to them.
+
+ <%= pf.check_box :covers_dpc, {checked: @payment_configuration.covers_dpc}, 1, 0 %>
+ <%= pf.label :covers_dpc, "Partner covers the DPC", class: 'c-input__label' %>
+
+
This setting controls whether <%= @tenant.short_name.presence || 'the institution'%> covers Large Data Fees for its users.
+
+ <%= pf.check_box :covers_ldf, {checked: @payment_configuration.covers_ldf}, 1, 0 %>
+ <%= pf.label :covers_ldf, "Partner covers Large Data Fees", class: 'c-input__label' %>
+
+
+ <%= pf.label :ldf_limit, "Covered Large Data Fee limit (GB)" %>
+ <%= pf.select :ldf_limit, ldf_pricing_tiers_options, { selected: @payment_configuration.ldf_limit }, class: 'c-input__select', disabled: !@payment_configuration.covers_ldf %>
+
+
+ <%= pf.label :yearly_ldf_limit, "Yearly Large Data Fee limit $" %>
+ <%= pf.number_field :yearly_ldf_limit, value: @payment_configuration.yearly_ldf_limit, class: 'c-input__text', disabled: !@payment_configuration.covers_ldf %>
+
+ <% end %>
diff --git a/app/views/stash_engine/tenant_admin/edit.js.erb b/app/views/stash_engine/tenant_admin/edit.js.erb
index 689dc8591f..d2cee4c662 100644
--- a/app/views/stash_engine/tenant_admin/edit.js.erb
+++ b/app/views/stash_engine/tenant_admin/edit.js.erb
@@ -19,5 +19,21 @@ var ldf = document.getElementById('payment_configuration_attributes_covers_ldf')
ldf.addEventListener('change', () => {
var limit = document.getElementById('payment_configuration_attributes_ldf_limit')
limit.disabled = !ldf.checked
- if (!ldf.checked) limit.value = ''
+ var amount_limit = document.getElementById('payment_configuration_attributes_yearly_ldf_limit')
+ amount_limit.disabled = !ldf.checked
+ if (!ldf.checked) {
+ limit.value = ''
+ amount_limit.value = ''
+ }
})
+
+var sponsor_id = document.getElementById('sponsor_id')
+sponsor_id.addEventListener('change', () => {
+ if (sponsor_id.value === '') {
+ document.getElementById('payment_notice').setAttribute('hidden', '');
+ document.getElementById('payment_container').removeAttribute('hidden');
+ } else {
+ document.getElementById('payment_container').setAttribute('hidden', '');
+ document.getElementById('payment_notice').removeAttribute('hidden');
+ }
+});
diff --git a/app/views/stash_engine/tenant_admin/new.js.erb b/app/views/stash_engine/tenant_admin/new.js.erb
index 88cd0fc087..aac059d06d 100644
--- a/app/views/stash_engine/tenant_admin/new.js.erb
+++ b/app/views/stash_engine/tenant_admin/new.js.erb
@@ -19,5 +19,21 @@ var ldf = document.getElementById('payment_configuration_attributes_covers_ldf')
ldf.addEventListener('change', () => {
var limit = document.getElementById('payment_configuration_attributes_ldf_limit')
limit.disabled = !ldf.checked
- if (!ldf.checked) limit.value = ''
+ var amount_limit = document.getElementById('payment_configuration_attributes_yearly_ldf_limit')
+ amount_limit.disabled = !ldf.checked
+ if (!ldf.checked) {
+ limit.value = ''
+ amount_limit.value = ''
+ }
})
+
+var sponsor_id = document.getElementById('sponsor_id')
+sponsor_id.addEventListener('change', () => {
+ if (sponsor_id.value === '') {
+ document.getElementById('payment_notice').setAttribute('hidden', '');
+ document.getElementById('payment_container').removeAttribute('hidden');
+ } else {
+ document.getElementById('payment_container').setAttribute('hidden', '');
+ document.getElementById('payment_notice').removeAttribute('hidden');
+ }
+});
diff --git a/config/initializers/constants.rb b/config/initializers/constants.rb
index 2bcc078c69..366d5d37c4 100644
--- a/config/initializers/constants.rb
+++ b/config/initializers/constants.rb
@@ -4,25 +4,6 @@
REPORTS_DIR = 'reports'.freeze
EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i
-# New fee calculator mapping keys to product names
-PRODUCT_NAME_MAPPER = {
- service_fee: 'Annual service fee',
- dpc_fee: 'Datasets count fee',
- invoice_fee: 'Invoice fee',
- storage_fee: 'Large data fee',
- storage_fee_overage: 'Large data fee overage',
- individual_storage_fee: 'Data Publishing Charge',
- total: 'Total',
- waiver_discount: 'Fee waiver discount',
- ppr_fee: 'Private for Peer Review Fee',
- ppr_discount: 'Paid Private for Peer Review Fee'
-}.freeze
-
-# New payment system error messages
-OLD_PAYMENT_SYSTEM_MESSAGE = 'Payer is not on 2025 payment plan'.freeze
-MISSING_PAYER_MESSAGE = 'Payer is missing. Please use individual calculator.'.freeze
-OUT_OF_RANGE_MESSAGE = 'The value is out of defined range'.freeze
-
SUBMISSION_QUEUE_NOTIFICATION_LIMIT = 10.freeze
SUBMISSION_QUEUE_NOTIFICATION_EVERY = 30.minutes.freeze
diff --git a/config/initializers/fees_constants.rb b/config/initializers/fees_constants.rb
new file mode 100644
index 0000000000..84acc38bdc
--- /dev/null
+++ b/config/initializers/fees_constants.rb
@@ -0,0 +1,129 @@
+# frozen_string_literal: true
+
+# New payment system error messages
+OLD_PAYMENT_SYSTEM_MESSAGE = 'Payer is not on 2025 payment plan'.freeze
+MISSING_PAYER_MESSAGE = 'Payer is missing. Please use individual calculator.'.freeze
+OUT_OF_RANGE_MESSAGE = 'The value is out of defined range'.freeze
+
+INVOICE_FEE = 199
+PPR_FEE = 50
+PPR_COUPON_ID = 'PPR_DISCOUNT_2025'.freeze
+
+# Waiver
+DISCOUNT_STORAGE_COUPON_ID = 'FEE_WAIVER_2025'.freeze
+FREE_STORAGE_SIZE = 10_000_000_000 # 10 GB
+
+# rubocop:disable Layout/SpaceInsideRangeLiteral, Layout/ExtraSpacing
+INDIVIDUAL_ESTIMATED_FILES_SIZE = if Rails.env.development? || Rails.env.dev?
+ # Values in MB
+ [
+ { tier: 1, range: 0.. 5_000_000, price: 150 },
+ { tier: 2, range: 5_000_001.. 10_000_000, price: 180 },
+ { tier: 3, range: 10_000_001.. 50_000_000, price: 520 },
+ { tier: 4, range: 50_000_001.. 100_000_000, price: 808 },
+ { tier: 5, range: 100_000_001.. 250_000_000, price: 1_750 },
+ { tier: 6, range: 250_000_001.. 500_000_000, price: 3_086 },
+ { tier: 7, range: 500_000_001..1_000_000_000, price: 6_077 },
+ { tier: 8, range: 1_000_000_001..2_000_000_000, price: 12_162 }
+ ].freeze
+else
+ # Values in GB
+ [
+ { tier: 1, range: 0.. 5_000_000_000, price: 150 },
+ { tier: 2, range: 5_000_000_001.. 10_000_000_000, price: 180 },
+ { tier: 3, range: 10_000_000_001.. 50_000_000_000, price: 520 },
+ { tier: 4, range: 50_000_000_001.. 100_000_000_000, price: 808 },
+ { tier: 5, range: 100_000_000_001.. 250_000_000_000, price: 1_750 },
+ { tier: 6, range: 250_000_000_001.. 500_000_000_000, price: 3_086 },
+ { tier: 7, range: 500_000_000_001..1_000_000_000_000, price: 6_077 },
+ { tier: 8, range: 1_000_000_000_001..2_000_000_000_000, price: 12_162 }
+ ].freeze
+end
+
+ESTIMATED_FILES_SIZE = if Rails.env.development? || Rails.env.dev?
+ # Values in MB
+ [
+ { tier: 0, range: 0.. 10_000_000, price: 0 },
+ { tier: 1, range: 10_000_001.. 50_000_000, price: 259 },
+ { tier: 2, range: 50_000_001.. 100_000_000, price: 464 },
+ { tier: 3, range: 100_000_001.. 250_000_000, price: 1_123 },
+ { tier: 4, range: 250_000_001.. 500_000_000, price: 2_153 },
+ { tier: 5, range: 500_000_001..1_000_000_000, price: 4_347 },
+ { tier: 6, range: 1_000_000_001..2_000_000_000, price: 8_809 }
+ ].freeze
+else
+ # Values in GB
+ [
+ { tier: 0, range: 0.. 10_000_000_000, price: 0 },
+ { tier: 1, range: 10_000_000_001.. 50_000_000_000, price: 259 },
+ { tier: 2, range: 50_000_000_001.. 100_000_000_000, price: 464 },
+ { tier: 3, range: 100_000_000_001.. 250_000_000_000, price: 1_123 },
+ { tier: 4, range: 250_000_000_001.. 500_000_000_000, price: 2_153 },
+ { tier: 5, range: 500_000_000_001..1_000_000_000_000, price: 4_347 },
+ { tier: 6, range: 1_000_000_000_001..2_000_000_000_000, price: 8_809 }
+ ].freeze
+ end
+
+ESTIMATED_DATASETS = [
+ { tier: 1, range: 0.. 5, price: 0 },
+ { tier: 2, range: 6.. 15, price: 1_650 },
+ { tier: 3, range: 16.. 25, price: 2_700 },
+ { tier: 4, range: 26.. 50, price: 5_350 },
+ { tier: 5, range: 51.. 75, price: 7_950 },
+ { tier: 6, range: 76..100, price: 10_500 },
+ { tier: 7, range: 101..150, price: 15_600 },
+ { tier: 8, range: 151..200, price: 20_500 },
+ { tier: 9, range: 201..250, price: 25_500 },
+ { tier: 10, range: 251..300, price: 30_250 },
+ { tier: 11, range: 301..350, price: 35_000 },
+ { tier: 12, range: 351..400, price: 39_500 },
+ { tier: 13, range: 401..450, price: 44_000 },
+ { tier: 14, range: 451..500, price: 48_750 },
+ { tier: 15, range: 501..550, price: 53_500 },
+ { tier: 16, range: 551..600, price: 58_250 }
+].freeze
+# rubocop:enable Layout/SpaceInsideRangeLiteral, Layout/ExtraSpacing
+
+NORMAL_SERVICE_FEE = [
+ { tier: 1, range: 0.. 100_000_000, price: 5_000 },
+ { tier: 2, range: 100_000_001.. 250_000_000, price: 10_000 },
+ { tier: 3, range: 250_000_001.. 500_000_000, price: 20_000 },
+ { tier: 4, range: 500_000_001.. 750_000_000, price: 30_000 },
+ { tier: 5, range: 750_000_001.. 1_000_000_000, price: 40_000 },
+ { tier: 6, range: 1_000_000_001..Float::INFINITY, price: 50_000 }
+].freeze
+
+LOW_MIDDLE_SERVICE_FEE = [
+ { tier: 1, range: 0.. 5_000_000, price: 1_000 },
+ { tier: 2, range: 5_000_001.. 25_000_000, price: 1_500 },
+ { tier: 3, range: 25_000_001.. 50_000_000, price: 2_500 },
+ { tier: 4, range: 50_000_001.. 100_000_000, price: 5_000 },
+ { tier: 5, range: 100_000_001..Float::INFINITY, price: 7_500 }
+].freeze
+
+PUBLISHER_SERVICE_FEE = [
+ { tier: 1, range: 0.. 500_000, price: 1_000 },
+ { tier: 2, range: 500_001.. 1_000_000, price: 2_500 },
+ { tier: 3, range: 1_000_001.. 5_000_000, price: 5_000 },
+ { tier: 4, range: 5_000_001.. 10_000_000, price: 7_500 },
+ { tier: 5, range: 10_000_001.. 25_000_000, price: 10_000 },
+ { tier: 6, range: 25_000_001.. 50_000_000, price: 12_500 },
+ { tier: 7, range: 50_000_001.. 100_000_000, price: 15_000 },
+ { tier: 8, range: 100_000_001.. 200_000_000, price: 22_500 },
+ { tier: 9, range: 200_000_001.. 500_000_000, price: 30_000 },
+ { tier: 10, range: 500_000_001..Float::INFINITY, price: 40_000 }
+].freeze
+
+# New fee calculator mapping keys to product names
+PRODUCT_NAME_MAPPER = {
+ service_fee: 'Annual service fee',
+ dpc_fee: 'Datasets count fee',
+ invoice_fee: 'Invoice fee',
+ storage_fee: 'Large data fee',
+ storage_fee_overage: 'Large data fee overage',
+ individual_storage_fee: 'Data Publishing Charge',
+ total: 'Total',
+ waiver_discount: 'Fee waiver discount',
+ ppr_fee: 'Private for Peer Review Fee',
+ ppr_discount: 'Paid Private for Peer Review Fee'
+}.freeze
diff --git a/db/data/20260220143851_populate_sponsored_payment_logs_sponsor_id.rb b/db/data/20260220143851_populate_sponsored_payment_logs_sponsor_id.rb
new file mode 100644
index 0000000000..3107ee5d97
--- /dev/null
+++ b/db/data/20260220143851_populate_sponsored_payment_logs_sponsor_id.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class PopulateSponsoredPaymentLogsSponsorId < ActiveRecord::Migration[8.0]
+ def up
+ SponsoredPaymentLog.where(sponsor_id: nil).each do |item|
+ item.update_column(:sponsor_id, item.payer&.sponsor_id)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/data_schema.rb b/db/data_schema.rb
index 7b69f1310d..70c86aa165 100644
--- a/db/data_schema.rb
+++ b/db/data_schema.rb
@@ -1 +1 @@
-DataMigrate::Data.define(version: 20260206142405)
+DataMigrate::Data.define(version: 20260220143851)
diff --git a/db/migrate/20260219130518_add_sponsor_id_to_sponsored_payment_logs.rb b/db/migrate/20260219130518_add_sponsor_id_to_sponsored_payment_logs.rb
new file mode 100644
index 0000000000..cef7d86bab
--- /dev/null
+++ b/db/migrate/20260219130518_add_sponsor_id_to_sponsored_payment_logs.rb
@@ -0,0 +1,6 @@
+class AddSponsorIdToSponsoredPaymentLogs < ActiveRecord::Migration[8.0]
+ def change
+ add_column :sponsored_payment_logs, :sponsor_id, :string
+ add_index :sponsored_payment_logs, :sponsor_id
+ end
+end
diff --git a/db/migrate/20260227153830_add_deleted_at_to_sponsored_payment_logs.rb b/db/migrate/20260227153830_add_deleted_at_to_sponsored_payment_logs.rb
new file mode 100644
index 0000000000..bc01e009bb
--- /dev/null
+++ b/db/migrate/20260227153830_add_deleted_at_to_sponsored_payment_logs.rb
@@ -0,0 +1,6 @@
+class AddDeletedAtToSponsoredPaymentLogs < ActiveRecord::Migration[8.0]
+ def change
+ add_column :sponsored_payment_logs, :deleted_at, :datetime
+ add_index :sponsored_payment_logs, :deleted_at
+ end
+end
diff --git a/spec/factories/sponsored_payment_logs.rb b/spec/factories/sponsored_payment_logs.rb
index 984940d9e7..2da765d2c4 100644
--- a/spec/factories/sponsored_payment_logs.rb
+++ b/spec/factories/sponsored_payment_logs.rb
@@ -3,6 +3,7 @@
# Table name: sponsored_payment_logs
#
# id :bigint not null, primary key
+# deleted_at :datetime
# dpc :integer
# ldf :integer
# payer_type :string(191)
@@ -10,22 +11,24 @@
# updated_at :datetime not null
# payer_id :string(191)
# resource_id :integer
+# sponsor_id :string(191)
#
# Indexes
#
+# index_sponsored_payment_logs_on_deleted_at (deleted_at)
# index_sponsored_payment_logs_on_payer_id_and_payer_type (payer_id,payer_type)
+# index_sponsored_payment_logs_on_sponsor_id (sponsor_id)
#
FactoryBot.define do
factory :sponsored_payment_log do
resource
- # payer
id { Faker::Number.number }
dpc { '1' }
ldf { nil }
payer_type { 'StashEngine::Tenant' }
payer_id { 'dryad' }
+ deleted_at { nil }
end
-
end
diff --git a/spec/factories/stash_engine/resources.rb b/spec/factories/stash_engine/resources.rb
index cc92eb3fa9..38dc9f3fe5 100644
--- a/spec/factories/stash_engine/resources.rb
+++ b/spec/factories/stash_engine/resources.rb
@@ -61,6 +61,7 @@
before(:create) do |resource, e|
user = e.user || StashEngine::User.find_by(id: resource.user_id) || create(:user)
+ user.tenant = resource.tenant if resource.tenant
resource.tenant_id = user.tenant_id
resource.current_editor_id = user.id unless resource.current_editor_id
end
diff --git a/spec/features/stash_datacite/review_dataset_spec.rb b/spec/features/stash_datacite/review_dataset_spec.rb
index 28d34f94e0..3653b9687b 100644
--- a/spec/features/stash_datacite/review_dataset_spec.rb
+++ b/spec/features/stash_datacite/review_dataset_spec.rb
@@ -105,6 +105,7 @@
fill_in_funder(name: 'Happy Clown School')
click_button 'Agreements'
+ sleep 15
expect(page).to have_text('Payment for this submission is sponsored by Happy Clown School')
end
diff --git a/spec/models/stash_engine/identifier_spec.rb b/spec/models/stash_engine/identifier_spec.rb
index 0a937653d4..cd553e0443 100644
--- a/spec/models/stash_engine/identifier_spec.rb
+++ b/spec/models/stash_engine/identifier_spec.rb
@@ -443,33 +443,86 @@ module StashEngine
end
describe '#user_must_pay?' do
- before(:each) do
- allow(@identifier).to receive(:institution_will_pay?).and_return(false)
- allow(@identifier).to receive(:funder_will_pay?).and_return(false)
+ let(:tenant) { create(:tenant, id: 'payer') }
+ let(:identifier) { create(:identifier) }
+ let(:new_size_limit) { 16_000_000_000 }
+ let!(:resource) { create(:resource, identifier: identifier, tenant: tenant, total_file_size: new_size_limit) }
+
+ subject { identifier.reload.user_must_pay? }
+
+ context 'when individual payment' do
+ it { is_expected.to be_truthy }
end
- it 'returns true if no one else will pay' do
- expect(@identifier.user_must_pay?).to eq(true)
+ context 'when individual payment has a waiver' do
+ let(:identifier) { create(:identifier, payment_type: 'waiver') }
+
+ it { is_expected.to be_truthy }
end
- it 'returns false if waiver is applied on an old_payment_system dataset' do
- @identifier.update(payment_type: 'waiver')
- expect(@identifier.user_must_pay?).to eq(true)
+ context 'when individual payment has a waiver on an old_payment_system dataset' do
+ let(:identifier) { create(:identifier, payment_type: 'waiver', old_payment_system: true) }
- @identifier.update(old_payment_system: true, payment_type: 'waiver')
- expect(@identifier.user_must_pay?).to eq(false)
+ it { is_expected.to be_falsey }
end
- it 'returns false if journal will pay' do
- journal = create(:journal, issn: @fake_issn)
- create(:payment_configuration, partner: journal, payment_plan: 'SUBSCRIPTION')
- journal.reload
- expect(@identifier.user_must_pay?).to eq(false)
+ context 'when journal will pay' do
+ let(:journal) { create(:journal, issn: @fake_issn) }
+ let!(:payment_configuration) { create(:payment_configuration, partner: journal, payment_plan: '2025', covers_dpc: true, covers_ldf: true) }
+ let!(:resource_publication) { create(:resource_publication, publication_issn: @fake_issn, resource: resource) }
+
+ it { is_expected.to be_falsey }
end
- it 'returns false if institution will pay' do
- allow(@identifier).to receive(:institution_will_pay?).and_return(true)
- expect(@identifier.user_must_pay?).to eq(false)
+ context 'when institution will pay' do
+ let(:size_limit) { nil }
+ let(:amount_limit) { nil }
+ let!(:payment_configuration) do
+ create(
+ :payment_configuration,
+ partner: tenant,
+ payment_plan: '2025',
+ covers_dpc: true,
+ covers_ldf: true,
+ ldf_limit: size_limit,
+ yearly_ldf_limit: amount_limit
+ )
+ end
+
+ it { is_expected.to be_falsey }
+
+ context 'when ldf size limit is not covered' do
+ let!(:payment_configuration) { create(:payment_configuration, partner: tenant, payment_plan: '2025', covers_dpc: true, covers_ldf: false) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when ldf size limit is not reached' do
+ let(:size_limit) { 2 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when ldf size limit is reached' do
+ let(:size_limit) { 2 }
+ let!(:new_size_limit) { 100_000_000_001 }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when ldf amount limit is not reached' do
+ let(:size_limit) { 2 }
+ let(:amount_limit) { 300 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when ldf amount limit is reached' do
+ let(:size_limit) { 1 }
+ let(:amount_limit) { 200 }
+
+ it { is_expected.to be_truthy }
+ end
end
end
diff --git a/spec/requests/api_controller_spec.rb b/spec/requests/api_controller_spec.rb
index fac3b8c02f..009093494e 100644
--- a/spec/requests/api_controller_spec.rb
+++ b/spec/requests/api_controller_spec.rb
@@ -91,7 +91,7 @@ module StashApi
end
after do
- ::File.delete(file_path) if ::File.exist?(file_path)
+ ::FileUtils.rm_f(file_path)
end
it 'returns error if file does not exist' do
diff --git a/spec/requests/stash_api/datasets_controller_spec.rb b/spec/requests/stash_api/datasets_controller_spec.rb
index 6c3f659d55..b7dd29ab41 100644
--- a/spec/requests/stash_api/datasets_controller_spec.rb
+++ b/spec/requests/stash_api/datasets_controller_spec.rb
@@ -942,7 +942,7 @@ module StashApi
it 'allows replacing of the metadata for a record' do
keys_to_extract = %w[title authors abstract]
- modified_metadata = @ds_info.select { |key, _| keys_to_extract.include?(key) }
+ modified_metadata = @ds_info.slice(*keys_to_extract)
modified_metadata['title'] = 'Crows wave goodbye'
modified_metadata['authors'].first['firstName'] = 'Helen'
modified_metadata['abstract'] = 'The implications of ambimorphic archetypes have been far-reaching and pervasive.'
@@ -958,7 +958,7 @@ module StashApi
it "doesn't allow non-auth users to update" do
keys_to_extract = %w[title authors abstract]
- modified_metadata = @ds_info.select { |key, _| keys_to_extract.include?(key) }
+ modified_metadata = @ds_info.slice(*keys_to_extract)
modified_metadata['title'] = 'Froozlotter'
response_code = put "/api/v2/datasets/#{CGI.escape(@ds_info['identifier'])}",
params: modified_metadata.to_json,
diff --git a/spec/services/cost_reporting_service_spec.rb b/spec/services/cost_reporting_service_spec.rb
index f5e735e8b3..0721449fc5 100644
--- a/spec/services/cost_reporting_service_spec.rb
+++ b/spec/services/cost_reporting_service_spec.rb
@@ -17,7 +17,7 @@
let(:tenant) { create(:tenant, campus_contacts: ['some@email.com'].to_json) }
let(:identifier) { create(:identifier) }
let!(:journal) { create(:journal) }
- let!(:payment_conf) { create(:payment_configuration, partner: journal, payment_plan: '2025') }
+ let!(:payment_conf) { create(:payment_configuration, partner: journal, payment_plan: '2025', covers_ldf: true) }
let(:prev_resource) do
create(:resource,
identifier: identifier,
diff --git a/spec/services/curation_service_spec.rb b/spec/services/curation_service_spec.rb
index 20d6d2d027..12b1a8dd46 100644
--- a/spec/services/curation_service_spec.rb
+++ b/spec/services/curation_service_spec.rb
@@ -469,7 +469,7 @@
end
describe '#processed_sponsored_resource' do
- %w[queued peer_review].each do |status|
+ %w[processing queued peer_review].each do |status|
it "calls log_payment if status is #{status}" do
service.process
@@ -478,7 +478,7 @@
end
end
- (StashEngine::CurationActivity.statuses.keys - %w[queued peer_review]).each do |status|
+ (StashEngine::CurationActivity.statuses.keys - %w[processing queued peer_review]).each do |status|
it "does not call log_payment if status is #{status}" do
service.process
diff --git a/spec/services/fee_calculator/waiver_service_spec.rb b/spec/services/fee_calculator/waiver_service_spec.rb
index 66c7a2611b..890072adb3 100644
--- a/spec/services/fee_calculator/waiver_service_spec.rb
+++ b/spec/services/fee_calculator/waiver_service_spec.rb
@@ -8,8 +8,8 @@ module FeeCalculator
subject { described_class.new(options, resource: resource).call }
before do
- stub_const 'FeeCalculator::WaiverService::DISCOUNT_STORAGE_COUPON_ID', coupon_id
- stub_const 'FeeCalculator::WaiverService::FREE_STORAGE_SIZE', 10_000_000_000 # 10GB
+ stub_const 'DISCOUNT_STORAGE_COUPON_ID', coupon_id
+ stub_const 'FREE_STORAGE_SIZE', 10_000_000_000 # 10GB
end
def no_charge_response(amount)
@@ -58,13 +58,13 @@ def no_discount_response(amount, invoice_fee: false)
end
context 'with storage_size at max free limit' do
- let(:new_files_size) { FeeCalculator::WaiverService::FREE_STORAGE_SIZE }
+ let(:new_files_size) { FREE_STORAGE_SIZE }
it { is_expected.to eq(no_charge_response(180)) }
end
context 'with storage_size over the max free limit' do
- let(:new_files_size) { FeeCalculator::WaiverService::FREE_STORAGE_SIZE + 1 }
+ let(:new_files_size) { FREE_STORAGE_SIZE + 1 }
it { is_expected.to eq(charge_response(520, 180)) }
end
@@ -110,13 +110,13 @@ def no_discount_response(amount, invoice_fee: false)
end
context 'with storage_size at max free limit' do
- let(:new_files_size) { FeeCalculator::WaiverService::FREE_STORAGE_SIZE }
+ let(:new_files_size) { FREE_STORAGE_SIZE }
it { is_expected.to eq(no_discount_response(0)) }
end
context 'with storage_size over the max free limit' do
- let(:new_files_size) { FeeCalculator::WaiverService::FREE_STORAGE_SIZE + 1 }
+ let(:new_files_size) { FREE_STORAGE_SIZE + 1 }
it { is_expected.to eq(no_discount_response(520 - 180)) }
end
@@ -164,13 +164,13 @@ def no_discount_response(amount, invoice_fee: false)
end
context 'with storage_size at max free limit' do
- let(:new_files_size) { FeeCalculator::WaiverService::FREE_STORAGE_SIZE }
+ let(:new_files_size) { FREE_STORAGE_SIZE }
it { is_expected.to eq(no_charge_response(180)) }
end
context 'with storage_size over the max free limit' do
- let(:new_files_size) { FeeCalculator::WaiverService::FREE_STORAGE_SIZE + 1 }
+ let(:new_files_size) { FREE_STORAGE_SIZE + 1 }
it { is_expected.to eq(charge_response(520, 180, invoice_fee: true)) }
end
@@ -208,13 +208,13 @@ def no_discount_response(amount, invoice_fee: false)
end
context 'with storage_size at max free limit' do
- let(:new_files_size) { FeeCalculator::WaiverService::FREE_STORAGE_SIZE }
+ let(:new_files_size) { FREE_STORAGE_SIZE }
it { is_expected.to eq(no_charges_response) }
end
context 'with storage_size over the max free limit' do
- let(:new_files_size) { FeeCalculator::WaiverService::FREE_STORAGE_SIZE + 1 }
+ let(:new_files_size) { FREE_STORAGE_SIZE + 1 }
it { is_expected.to eq(no_discount_response(520 - 180, invoice_fee: true)) }
end
diff --git a/spec/services/payers_service_spec.rb b/spec/services/payers_service_spec.rb
index 5d34928b69..664b3e2e6a 100644
--- a/spec/services/payers_service_spec.rb
+++ b/spec/services/payers_service_spec.rb
@@ -1,5 +1,6 @@
describe PayersService do
let(:tenant) { create(:tenant) }
+ let(:sponsored_tenant) { create(:tenant, id: 'sponsored', sponsor: tenant) }
subject { PayersService.new(tenant) }
@@ -25,5 +26,44 @@
it { is_expected.to be_falsey }
end
end
+
+ context 'when is sponsored' do
+ let!(:payment_configuration) { create(:payment_configuration, partner: tenant, payment_plan: '2025') }
+
+ it 'returns true' do
+ expect(PayersService.new(tenant).is_2025_payer?).to be_truthy
+ expect(PayersService.new(sponsored_tenant).is_2025_payer?).to be_truthy
+ end
+ end
+ end
+
+ describe '#payment_sponsor' do
+ subject { PayersService.new(tenant).payment_sponsor }
+
+ context 'when is nil' do
+ it 'returns true' do
+ expect(PayersService.new(nil).payment_sponsor).to be_nil
+ end
+ end
+
+ context 'when is not sponsored' do
+ it 'returns self' do
+ expect(PayersService.new(tenant).payment_sponsor).to eq(tenant)
+ end
+ end
+
+ context 'when is sponsored' do
+ it 'returns the sponsor' do
+ expect(PayersService.new(sponsored_tenant).payment_sponsor).to eq(tenant)
+ end
+ end
+
+ context 'when is payer does not have payment_sponsor defined' do
+ let(:user) { create(:user) }
+
+ it 'returns self' do
+ expect(PayersService.new(user).payment_sponsor).to eq(user)
+ end
+ end
end
end
diff --git a/spec/services/payment_limits_service_spec.rb b/spec/services/payment_limits_service_spec.rb
index 3da04bd1bf..4cfa49d118 100644
--- a/spec/services/payment_limits_service_spec.rb
+++ b/spec/services/payment_limits_service_spec.rb
@@ -1,97 +1,225 @@
describe PaymentLimitsService do
- let(:identifier) { create(:identifier) }
- let(:total_file_size) { 10_000_000_001 }
- let!(:tenant) { create(:tenant) }
- let!(:payment_conf) { create(:payment_configuration, partner: tenant, payment_plan: '2025', covers_dpc: true, yearly_ldf_limit: 1_000) }
- let(:resource) { create(:resource, identifier: identifier, tenant: tenant, total_file_size: total_file_size) }
- let(:payer) { tenant }
-
- let(:subject) { PaymentLimitsService.new(resource, payer) }
-
- describe '#initialize' do
- it 'sets proper attributes' do
- expect(subject.resource).to eq(resource)
- expect(subject.payer).to eq(payer)
+ describe 'payer is an institution' do
+ let(:identifier) { create(:identifier) }
+ let(:total_file_size) { 10_000_000_001 }
+ let!(:sponsor_tenant) { create(:tenant, id: 'sponsor') }
+ let!(:tenant) { create(:tenant, sponsor: sponsor_tenant, id: 'payer') }
+ let!(:other_tenant) { create(:tenant, sponsor: sponsor_tenant, id: 'other') }
+ let!(:payment_conf) do
+ create(:payment_configuration,
+ partner: sponsor_tenant,
+ payment_plan: '2025',
+ covers_dpc: true,
+ covers_ldf: true,
+ ldf_limit: 1,
+ yearly_ldf_limit: 1_000)
end
- end
+ let(:resource) { create(:resource, identifier: identifier, tenant: tenant, total_file_size: total_file_size) }
+ let(:payer) { sponsor_tenant }
- describe '#limits_exceeded?' do
- let(:subject) { PaymentLimitsService.new(resource, payer).limits_exceeded? }
+ let(:subject) { PaymentLimitsService.new(resource, payer) }
- context 'when payer has a 2025 payment plan' do
- context 'when limit is set' do
- context 'but not reached' do
- it { is_expected.to be_falsey }
- end
+ describe '#initialize' do
+ it 'sets proper attributes' do
+ expect(subject.resource).to eq(resource)
+ expect(subject.payer).to eq(payer)
+ end
+ end
- context 'when limit is already exceeded' do
- let!(:log) { create(:sponsored_payment_log, payer: tenant, ldf: 1_001) }
+ describe '#limits_exceeded?' do
+ let(:subject) { PaymentLimitsService.new(resource, payer).limits_exceeded? }
- it { is_expected.to be_truthy }
+ context 'when payer has a 2025 payment plan' do
+ context 'when payer amount limit is set' do
+ context 'but not reached' do
+ it { is_expected.to be_falsey }
+ end
+
+ context 'when limit is already exceeded' do
+ let!(:log) { create(:sponsored_payment_log, payer: tenant, ldf: 1_001, sponsor_id: sponsor_tenant.id) }
+
+ it { is_expected.to be_truthy }
+
+ context 'but resource LDF is 0' do
+ let(:total_file_size) { 10_000 }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'when limit is not reached but with current resource it will be exceeded' do
+ let!(:log) { create(:sponsored_payment_log, payer: tenant, ldf: 999, sponsor_id: sponsor_tenant.id) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when limit is not reached and with current resource it will not be exceeded' do
+ let!(:log) { create(:sponsored_payment_log, payer: tenant, ldf: 100, sponsor_id: sponsor_tenant.id) }
+
+ it { is_expected.to be_falsey }
+ end
context 'but resource LDF is 0' do
let(:total_file_size) { 10_000 }
it { is_expected.to be_falsey }
end
- end
- context 'when limit is not reached but with current resource it will be exceeded' do
- let!(:log) { create(:sponsored_payment_log, payer: tenant, ldf: 999) }
+ context 'with logs on previous year' do
+ let!(:log) { create(:sponsored_payment_log, payer: tenant, ldf: 1_001, created_at: 1.year.ago, sponsor_id: sponsor_tenant.id) }
- it { is_expected.to be_truthy }
+ it { is_expected.to be_falsey }
+ end
end
- context 'when limit is not reached and with current resource it will not be exceeded' do
- let!(:log) { create(:sponsored_payment_log, payer: tenant, ldf: 100) }
+ context 'when sponsor amount limit is set' do
+ context 'but not reached' do
+ it { is_expected.to be_falsey }
+ end
- it { is_expected.to be_falsey }
+ context 'when limit is already exceeded' do
+ let!(:log) { create(:sponsored_payment_log, payer: other_tenant, ldf: 1_001, sponsor_id: sponsor_tenant.id) }
+
+ it { is_expected.to be_truthy }
+
+ context 'but resource LDF is 0' do
+ let(:total_file_size) { 10_000 }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'and payer has no limit set' do
+ let!(:payment_conf) do
+ create(:payment_configuration,
+ partner: sponsor_tenant,
+ payment_plan: '2025',
+ covers_dpc: true,
+ covers_ldf: true,
+ ldf_limit: nil,
+ yearly_ldf_limit: nil)
+ end
+
+ context 'when amounts are already huge' do
+ let!(:log) { create(:sponsored_payment_log, payer: other_tenant, ldf: 1_000_001, sponsor_id: sponsor_tenant.id) }
+
+ it { is_expected.to be_falsey }
+
+ context 'with resource LDF is 0' do
+ let(:total_file_size) { 10_000 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'new files size is huge' do
+ let(:total_file_size) { 10_000_000_001 }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ context 'and payer has cover ldf off' do
+ let!(:payment_conf) do
+ create(:payment_configuration,
+ partner: sponsor_tenant,
+ payment_plan: '2025',
+ covers_dpc: true,
+ covers_ldf: false,
+ ldf_limit: nil,
+ yearly_ldf_limit: nil)
+ end
+
+ context 'when limit is already exceeded' do
+ let!(:log) { create(:sponsored_payment_log, payer: other_tenant, ldf: 1_001, sponsor_id: sponsor_tenant.id) }
+
+ it { is_expected.to be_truthy }
+
+ context 'but resource LDF is 0' do
+ let(:total_file_size) { 10_000 }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ context 'when limit is not reached but with current resource it will be exceeded' do
+ let!(:log) { create(:sponsored_payment_log, payer: other_tenant, ldf: 999, sponsor_id: sponsor_tenant.id) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when limit is not reached and with current resource it will not be exceeded' do
+ let!(:log) { create(:sponsored_payment_log, payer: other_tenant, ldf: 100, sponsor_id: sponsor_tenant.id) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'but resource LDF is 0' do
+ let(:total_file_size) { 10_000 }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'with logs on previous year' do
+ let!(:log) { create(:sponsored_payment_log, payer: tenant, ldf: 1_001, created_at: 1.year.ago, sponsor_id: sponsor_tenant.id) }
+ let!(:log2) { create(:sponsored_payment_log, payer: other_tenant, ldf: 1_001, created_at: 1.year.ago, sponsor_id: sponsor_tenant.id) }
+
+ it { is_expected.to be_falsey }
+ end
end
- context 'but resource LDF is 0' do
- let(:total_file_size) { 10_000 }
+ context 'when dataset size limit is set' do
+ context 'but not reached' do
+ it { is_expected.to be_falsey }
+ end
- it { is_expected.to be_falsey }
+ context 'when limit is not reached but with current version it will be exceeded' do
+ let!(:log) { create(:sponsored_payment_log, payer: tenant, ldf: 900, sponsor_id: sponsor_tenant.id) }
+ let(:total_file_size) { 50_000_000_001 }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when limit is not reached and with current resource it will not be exceeded' do
+ let(:total_file_size) { 50_000_000_000 }
+
+ it { is_expected.to be_falsey }
+ end
end
- context 'with logs on previous year' do
- let!(:log) { create(:sponsored_payment_log, payer: tenant, ldf: 1_001, created_at: 1.year.ago) }
+ context 'when no limit is set' do
+ let!(:payment_conf) do
+ create(:payment_configuration, partner: tenant, payment_plan: '2025', covers_dpc: true, covers_ldf: true, yearly_ldf_limit: nil)
+ end
it { is_expected.to be_falsey }
end
end
- context 'when no limit is set' do
- let!(:payment_conf) do
- create(:payment_configuration, partner: tenant, payment_plan: '2025', covers_dpc: true, yearly_ldf_limit: nil)
- end
-
- it { is_expected.to be_falsey }
+ context 'when payer is not set' do
+ let(:payer) { nil }
+ it { is_expected.to be_truthy }
end
- end
- context 'when payer is not set' do
- let(:payer) { nil }
- it { is_expected.to be_truthy }
- end
+ context 'when payer has different payment plan than 2025' do
+ let!(:payment_conf) { create(:payment_configuration, partner: tenant, payment_plan: 'TIERED', covers_dpc: true) }
- context 'when payer has different payment plan than 2025' do
- let!(:payment_conf) { create(:payment_configuration, partner: tenant, payment_plan: 'TIERED', covers_dpc: true) }
-
- it { is_expected.to be_falsey }
+ it { is_expected.to be_falsey }
+ end
end
- end
- describe '#payment_allowed?' do
- it 'returns true if limit is exceeded' do
- expect(subject).to receive(:limits_exceeded?).and_return(true)
- expect(subject.payment_allowed?).to be_falsey
- end
+ describe '#payment_allowed?' do
+ it 'returns true if limit is exceeded' do
+ expect(subject).to receive(:limits_exceeded?).and_return(true)
+ expect(subject.payment_allowed?).to be_falsey
+ end
- it 'returns false if limit is not exceeded' do
- expect(subject).to receive(:limits_exceeded?).and_return(false)
- expect(subject.payment_allowed?).to be_truthy
+ it 'returns false if limit is not exceeded' do
+ expect(subject).to receive(:limits_exceeded?).and_return(false)
+ expect(subject.payment_allowed?).to be_truthy
+ end
end
end
end
diff --git a/spec/services/sponsored_payments_service_spec.rb b/spec/services/sponsored_payments_service_spec.rb
index c391060c2a..b7d24bf464 100644
--- a/spec/services/sponsored_payments_service_spec.rb
+++ b/spec/services/sponsored_payments_service_spec.rb
@@ -14,8 +14,11 @@
let(:identifier) { create(:identifier) }
let(:total_file_size) { 10_000_000_001 }
let!(:tenant) { create(:tenant) }
- let!(:payment_conf) { create(:payment_configuration, partner: tenant, payment_plan: '2025', covers_dpc: true, yearly_ldf_limit: 1_000) }
+ let!(:payment_conf) do
+ create(:payment_configuration, partner: tenant, payment_plan: '2025', covers_dpc: true, covers_ldf: true, yearly_ldf_limit: 1_000)
+ end
let(:resource) { create(:resource, identifier: identifier, tenant: tenant, total_file_size: total_file_size) }
+ # before { identifier.reload }
let(:subject) { SponsoredPaymentsService.new(resource) }
@@ -42,33 +45,377 @@
end
context 'when payer exists with a 2025 payment plan' do
- context 'and a payment record exists' do
- include_examples('creates sponsored payment log')
+ context 'when LDF amount limit is reached' do
+ context 'and a payment record exists' do
+ include_examples('creates sponsored payment log')
+
+ it 'has correct info' do
+ subject.log_payment
+
+ expect(tenant.payment_logs.count).to eq(1)
+ expect(tenant.payment_logs.last.attributes).to include(
+ {
+ resource_id: resource.id,
+ payer_id: tenant.id,
+ payer_type: tenant.class.name,
+ ldf: 259,
+ sponsor_id: tenant.id
+ }.stringify_keys
+ )
+ end
+ end
+
+ context 'when tenant has a sponsor' do
+ let!(:sponsor_tenant) { create(:tenant, id: 'sponsor') }
+ let!(:tenant) { create(:tenant, id: 'payer', sponsor_id: sponsor_tenant.id) }
+ let!(:payment_conf) do
+ create(:payment_configuration, partner: sponsor_tenant, payment_plan: '2025', covers_dpc: true, covers_ldf: true, yearly_ldf_limit: 1_000)
+ end
+
+ include_examples('creates sponsored payment log')
+
+ it 'has correct info' do
+ subject.log_payment
+
+ expect(tenant.payment_logs.count).to eq(1)
+ expect(tenant.payment_logs.last.attributes).to include(
+ {
+ resource_id: resource.id,
+ payer_id: tenant.id,
+ payer_type: tenant.class.name,
+ ldf: 259,
+ sponsor_id: sponsor_tenant.id
+ }.stringify_keys
+ )
+ end
+ end
end
- context 'and a invoice payment record exists' do
- let!(:payment) { create(:resource_payment, resource: resource, pay_with_invoice: true) }
+ context 'when LDF size is not reached' do
+ let!(:payment_conf) do
+ create(:payment_configuration, partner: tenant, payment_plan: '2025', covers_dpc: true, covers_ldf: true, ldf_limit: 5)
+ end
- include_examples('does not create sponsored payment log')
+ context 'and a payment record exists' do
+ include_examples('creates sponsored payment log')
+
+ it 'has correct info' do
+ subject.log_payment
+
+ expect(tenant.payment_logs.count).to eq(1)
+ expect(tenant.payment_logs.last.attributes).to include(
+ {
+ resource_id: resource.id,
+ payer_id: tenant.id,
+ payer_type: tenant.class.name,
+ ldf: 259,
+ sponsor_id: tenant.id
+ }.stringify_keys
+ )
+ end
+ end
+
+ context 'when tenant has a sponsor' do
+ let!(:sponsor_tenant) { create(:tenant, id: 'sponsor') }
+ let!(:tenant) { create(:tenant, id: 'payer', sponsor_id: sponsor_tenant.id) }
+ let!(:payment_conf) do
+ create(:payment_configuration, partner: sponsor_tenant, payment_plan: '2025', covers_dpc: true, covers_ldf: true, ldf_limit: 5)
+ end
+
+ include_examples('creates sponsored payment log')
+
+ it 'has correct info' do
+ subject.log_payment
+
+ expect(tenant.payment_logs.count).to eq(1)
+ expect(tenant.payment_logs.last.attributes).to include(
+ {
+ resource_id: resource.id,
+ payer_id: tenant.id,
+ payer_type: tenant.class.name,
+ ldf: 259,
+ sponsor_id: sponsor_tenant.id
+ }.stringify_keys
+ )
+ end
+ end
end
- context 'and a CC payment record exists' do
- context 'when payment succeeded' do
- let!(:payment) { create(:resource_payment, resource: resource, pay_with_invoice: false, status: :paid) }
+ context 'when LDF size is reached' do
+ let(:total_file_size) { 50_000_000_001 }
+ let!(:payment_conf) do
+ create(:payment_configuration, partner: tenant, payment_plan: '2025', covers_dpc: true, covers_ldf: true, ldf_limit: 1)
+ end
- include_examples('does not create sponsored payment log')
+ context 'and a payment record exists' do
+ include_examples('creates sponsored payment log')
+
+ it 'has correct info' do
+ subject.log_payment
+
+ expect(tenant.payment_logs.count).to eq(1)
+ expect(tenant.payment_logs.last.attributes).to include(
+ {
+ resource_id: resource.id,
+ payer_id: tenant.id,
+ payer_type: tenant.class.name,
+ ldf: 259,
+ sponsor_id: tenant.id
+ }.stringify_keys
+ )
+ end
+ end
+
+ context 'when tenant has a sponsor' do
+ let!(:sponsor_tenant) { create(:tenant, id: 'sponsor') }
+ let!(:tenant) { create(:tenant, id: 'payer', sponsor_id: sponsor_tenant.id) }
+ let!(:payment_conf) do
+ create(:payment_configuration, partner: sponsor_tenant, payment_plan: '2025', covers_dpc: true, covers_ldf: true, ldf_limit: 2)
+ end
+
+ include_examples('creates sponsored payment log')
+
+ it 'has correct info' do
+ subject.log_payment
+
+ expect(tenant.payment_logs.count).to eq(1)
+ expect(tenant.payment_logs.last.attributes).to include(
+ {
+ resource_id: resource.id,
+ payer_id: tenant.id,
+ payer_type: tenant.class.name,
+ ldf: 464,
+ sponsor_id: sponsor_tenant.id
+ }.stringify_keys
+ )
+ end
end
+ end
- context 'when payment failed' do
- let!(:payment) { create(:resource_payment, resource: resource, pay_with_invoice: false, status: :failed) }
+ context 'does not create a log if amount is 0' do
+ context 'when ldf is in free tier' do
+ let(:total_file_size) { 9_000_000_001 }
include_examples('does not create sponsored payment log')
end
- context 'when payment is just created' do
- let!(:payment) { create(:resource_payment, resource: resource, pay_with_invoice: false, status: :created) }
+ context 'when ldf is already paid on previous resource' do
+ let(:resource) { create(:resource, identifier: identifier, tenant: tenant, total_file_size: total_file_size, created_at: 1.minute.ago) }
- include_examples('creates sponsored payment log')
+ it 'has correct info' do
+ subject.log_payment
+
+ expect(tenant.payment_logs.count).to eq(1)
+ expect(tenant.payment_logs.last.attributes).to include(
+ {
+ resource_id: resource.id,
+ payer_id: tenant.id,
+ payer_type: tenant.class.name,
+ ldf: 259,
+ sponsor_id: tenant.id
+ }.stringify_keys
+ )
+
+ new_resource = create(:resource, identifier: identifier, tenant: tenant, total_file_size: 11_000_000_000)
+ expect { SponsoredPaymentsService.new(new_resource).log_payment }.not_to(change { SponsoredPaymentLog.count })
+ end
+ end
+ end
+
+ context 'if files are deleted' do
+ let!(:payment_conf) do
+ create(:payment_configuration, partner: tenant, payment_plan: '2025', covers_dpc: true, covers_ldf: true)
+ end
+ let(:identifier) { create(:identifier, last_invoiced_file_size: 110_000_000_000) }
+
+ context 'when each tier has its own log' do
+ let!(:res1) { create(:resource, identifier: identifier, tenant: tenant, total_file_size: 70_000_000_000, created_at: 10.minute.ago) }
+ let!(:ca1) { create(:curation_activity, status: :queued, resource: res1) }
+ let!(:log1) { create(:sponsored_payment_log, resource: res1, ldf: 464, payer: tenant, sponsor_id: tenant.id) }
+ let!(:res2) { create(:resource, identifier: identifier, tenant: tenant, total_file_size: 110_000_000_000, created_at: 9.minute.ago) }
+ let!(:ca2) { create(:curation_activity, status: :queued, resource: res2) }
+ let!(:log2) { create(:sponsored_payment_log, resource: res2, ldf: 1_123, payer: tenant, sponsor_id: tenant.id) }
+
+ context 'when ldf is in the same tier' do
+ let(:total_file_size) { 105_000_000_000 }
+
+ include_examples('does not create sponsored payment log')
+
+ it 'updates last_invoiced_file_size' do
+ subject.log_payment
+
+ expect(identifier.reload.last_invoiced_file_size).to eq(105_000_000_000)
+ end
+ end
+
+ context 'when ldf is one tier lower' do
+ let(:total_file_size) { 60_000_000_000 }
+
+ it 'deletes one log and does not create a new one' do
+ expect(log2.reload.deleted?).to be_falsey
+ expect(tenant.payment_logs.count).to eq(2)
+ subject.log_payment
+
+ expect(log2.reload.deleted?).to be_truthy
+ expect(tenant.payment_logs.count).to eq(1)
+ end
+
+ it 'updates last_invoiced_file_size' do
+ subject.log_payment
+
+ expect(identifier.reload.last_invoiced_file_size).to eq(60_000_000_000)
+ end
+ end
+
+ context 'when ldf is 2 tier lower each having there own logs' do
+ let(:total_file_size) { 15_000_000_000 }
+
+ it 'deletes both logs and creates a new one' do
+ expect(log1.reload.deleted?).to be_falsey
+ expect(log2.reload.deleted?).to be_falsey
+ expect(tenant.payment_logs.count).to eq(2)
+ subject.log_payment
+
+ expect(log1.reload.deleted?).to be_truthy
+ expect(log2.reload.deleted?).to be_truthy
+ expect(tenant.payment_logs.count).to eq(1)
+ end
+
+ context 'when something fails' do
+ it 'does not delete anything' do
+ allow(SponsoredPaymentLog).to receive(:create).and_raise(ActiveRecord::RecordInvalid)
+
+ expect(tenant.payment_logs.count).to eq(2)
+ expect { subject.log_payment }.to raise_error(ActiveRecord::RecordInvalid)
+ expect(tenant.payment_logs.count).to eq(2)
+ end
+ end
+
+ it 'updates last_invoiced_file_size' do
+ subject.log_payment
+
+ expect(identifier.reload.last_invoiced_file_size).to eq(15_000_000_000)
+ end
+
+ context 'when there is an intermediary published version' do
+ let!(:ca) { create(:curation_activity, status: :published, resource: res1) }
+
+ it 'deletes newer logs and stops at the published one and does not create any new log' do
+ expect(log2.reload.deleted?).to be_falsey
+ expect(tenant.payment_logs.count).to eq(2)
+ subject.log_payment
+
+ expect(log1.reload.deleted?).to be_falsey
+ expect(log2.reload.deleted?).to be_truthy
+ expect(tenant.payment_logs.count).to eq(1)
+ end
+ end
+ end
+
+ context 'when goes back to free tier' do
+ let(:total_file_size) { 5_000_000_000 }
+
+ it 'deletes both logs log' do
+ expect(tenant.payment_logs.count).to eq(2)
+ subject.log_payment
+
+ expect(tenant.payment_logs.count).to eq(0)
+ end
+
+ it 'updates last_invoiced_file_size' do
+ subject.log_payment
+
+ expect(identifier.reload.last_invoiced_file_size).to eq(5_000_000_000)
+ end
+ end
+ end
+
+ context 'when there is only one log' do
+ let!(:res1) { create(:resource, identifier: identifier, tenant: tenant, total_file_size: 20_000_000_000, created_at: 10.minute.ago) }
+ let!(:ca1) { create(:curation_activity, status: :queued, resource: res1) }
+ let!(:log1) { create(:sponsored_payment_log, resource: res1, ldf: 259, payer: tenant, sponsor_id: tenant.id) }
+ let!(:res2) { create(:resource, identifier: identifier, tenant: tenant, total_file_size: 110_000_000_000, created_at: 9.minute.ago) }
+ let!(:ca2) { create(:curation_activity, status: :queued, resource: res2) }
+ let!(:log2) { create(:sponsored_payment_log, resource: res2, ldf: 1_123, payer: tenant, sponsor_id: tenant.id) }
+
+ context 'when ldf is in the same tier' do
+ let(:total_file_size) { 105_000_000_000 }
+
+ include_examples('does not create sponsored payment log')
+
+ it 'updates last_invoiced_file_size' do
+ subject.log_payment
+
+ expect(identifier.reload.last_invoiced_file_size).to eq(105_000_000_000)
+ end
+ end
+
+ context 'when ldf is one tier lower' do
+ let(:total_file_size) { 70_000_000_000 }
+
+ it 'deletes one log and creates a new one' do
+ expect(log2.reload.deleted?).to be_falsey
+ expect(tenant.payment_logs.count).to eq(2)
+ subject.log_payment
+
+ expect(log1.reload.deleted?).to be_falsey
+ expect(log2.reload.deleted?).to be_truthy
+ # creates a new log
+ expect(tenant.payment_logs.count).to eq(2)
+ expect(tenant.payment_logs.last.attributes).to include(
+ {
+ resource_id: resource.id,
+ payer_id: tenant.id,
+ payer_type: tenant.class.name,
+ ldf: 464 - 259,
+ sponsor_id: tenant.id
+ }.stringify_keys
+ )
+ end
+
+ it 'updates last_invoiced_file_size' do
+ subject.log_payment
+
+ expect(identifier.reload.last_invoiced_file_size).to eq(70_000_000_000)
+ end
+ end
+
+ context 'when ldf is 2 tier lower and there is already a log on this tier' do
+ let(:total_file_size) { 15_000_000_000 }
+
+ it 'deletes larger tier log' do
+ expect(log2.reload.deleted?).to be_falsey
+ expect(tenant.payment_logs.count).to eq(2)
+ subject.log_payment
+
+ expect(log1.reload.deleted?).to be_falsey
+ expect(log2.reload.deleted?).to be_truthy
+ expect(tenant.payment_logs.count).to eq(1)
+ end
+
+ it 'updates last_invoiced_file_size' do
+ subject.log_payment
+
+ expect(identifier.reload.last_invoiced_file_size).to eq(15_000_000_000)
+ end
+ end
+
+ context 'when goes back to free tier' do
+ let(:total_file_size) { 5_000_000_000 }
+
+ it 'deletes both logs log' do
+ expect(tenant.payment_logs.count).to eq(2)
+ subject.log_payment
+
+ expect(tenant.payment_logs.count).to eq(0)
+ end
+
+ it 'updates last_invoiced_file_size' do
+ subject.log_payment
+
+ expect(identifier.reload.last_invoiced_file_size).to eq(5_000_000_000)
+ end
+ end
end
end
end
diff --git a/spec/shared_examples/fee_calculator.rb b/spec/shared_examples/fee_calculator.rb
index d80f069e38..cc655b837f 100644
--- a/spec/shared_examples/fee_calculator.rb
+++ b/spec/shared_examples/fee_calculator.rb
@@ -41,19 +41,19 @@
context 'when limit tier is set' do
let(:covers_ldf) { true }
- context 'with limit over the new file size' do
+ context 'with limit is over the new file size' do
let(:ldf_limit) { 3 }
it { is_expected.to include(no_charges_response) }
end
- context 'with limit equal to the new file size' do
+ context 'with limit is equal to the new file size' do
let(:ldf_limit) { 2 }
it { is_expected.to include(no_charges_response) }
end
- context 'with limit over the new file size' do
+ context 'with limit is under the new file size' do
let(:ldf_limit) { 1 }
it { is_expected.to include({ storage_fee: 205, storage_fee_label: 'Large data fee overage' }) }