Skip to content
Open
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
4d57fd3
add sponsor limits
alinvetian Feb 20, 2026
1dbbd33
rubocop fix
alinvetian Feb 20, 2026
c9cc84f
rubocop fix
alinvetian Feb 20, 2026
39fbaf4
update payment forms
alinvetian Feb 20, 2026
97678d8
udate specs
alinvetian Feb 23, 2026
ddbb7e3
added more specs
alinvetian Feb 23, 2026
e6cdaeb
payer must cover LDF
alinvetian Feb 24, 2026
85aa020
Merge branch 'main' of github.com:datadryad/dryad-app into 4817-conso…
alinvetian Feb 24, 2026
edbceb4
rubocop
alinvetian Feb 24, 2026
e8a2cdb
specs fixes
alinvetian Feb 24, 2026
9379a50
payment system updates
alinvetian Feb 25, 2026
e174332
specs fixes
alinvetian Feb 26, 2026
ec2e292
Merge branch 'main' of github.com:datadryad/dryad-app into 4817-conso…
alinvetian Feb 26, 2026
8daf2da
rollback change
alinvetian Feb 26, 2026
80f0cb1
logging updates and tests
alinvetian Feb 26, 2026
e67242d
Merge branch 'main' of github.com:datadryad/dryad-app into 4817-conso…
alinvetian Feb 27, 2026
c2330d0
handle version delete
alinvetian Feb 27, 2026
72fb715
handle files deletion on newer versions
alinvetian Feb 27, 2026
a5ed585
changing sponsor, resets file size and sponsored payments
alinvetian Mar 2, 2026
f907b34
Merge pull request #2780 from datadryad/4878-missing-consideration-fo…
alinvetian Mar 2, 2026
ef7309d
fixed merge conflicts
alinvetian Mar 2, 2026
9414506
update bad response
alinvetian Mar 3, 2026
c6a9057
Merge branch 'main' of github.com:datadryad/dryad-app into 4817-conso…
alinvetian Mar 5, 2026
5a04485
File size limits are in MB for dev
alinvetian Mar 5, 2026
0265000
rubocop
alinvetian Mar 5, 2026
5e35d08
update tests
alinvetian Mar 5, 2026
b0f755c
update tests
alinvetian Mar 5, 2026
741bc85
Update tenant connect_list
alinvetian Mar 17, 2026
3f18de2
Merge branch 'main' of github.com:datadryad/dryad-app into 4817-conso…
alinvetian Mar 17, 2026
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
5 changes: 4 additions & 1 deletion app/models/payment_configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions app/models/sponsored_payment_log.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,24 @@
# Table name: sponsored_payment_logs
#
# id :bigint not null, primary key
# deleted_at :datetime
# dpc :integer
# ldf :integer
# payer_type :string(191)
# created_at :datetime not null
# 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
Expand Down
3 changes: 2 additions & 1 deletion app/models/stash_engine/identifier.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -594,7 +595,7 @@ def last_invoiced_file_size
return if res.nil? || res.total_file_size.nil?

update(last_invoiced_file_size: res.total_file_size)
res
res.total_file_size
end

# ------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions app/models/stash_engine/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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? }]
Expand Down
8 changes: 5 additions & 3 deletions app/models/stash_engine/support/payment_methods.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions app/models/stash_engine/tenant.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,5 +114,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
2 changes: 1 addition & 1 deletion app/services/curation_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 40 additions & 7 deletions app/services/fee_calculator/base_service.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# rubocop:disable Metrics/ClassLength

module FeeCalculator
class BaseService
attr_reader :options, :resource
Expand Down Expand Up @@ -41,6 +43,7 @@ def initialize(options = {}, resource: nil, payer_record: nil)
@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
Expand All @@ -53,11 +56,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
Expand Down Expand Up @@ -90,10 +105,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
Expand Down Expand Up @@ -192,12 +230,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)
Expand Down Expand Up @@ -241,3 +273,4 @@ def add_coupon(coupon_id)
end
end
end
# rubocop:enable Metrics/ClassLength
16 changes: 16 additions & 0 deletions app/services/fee_calculator_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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(
FeeCalculator::BaseService::ESTIMATED_FILES_SIZE,
payer.payment_configuration.ldf_limit
)
end

def calculator_service
calculator_class.constantize
end

private

def calculator_class
Expand Down
8 changes: 7 additions & 1 deletion app/services/payers_service.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
63 changes: 55 additions & 8 deletions app/services/payment_limits_service.rb
Original file line number Diff line number Diff line change
@@ -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
Loading