Skip to content

Commit

Permalink
[ATG / Medbridge] Support Salesforce Precision proration prices (#1239)
Browse files Browse the repository at this point in the history
  • Loading branch information
nadaismail-stripe authored Nov 14, 2023
1 parent 5be2ece commit e0fed7e
Show file tree
Hide file tree
Showing 10 changed files with 31,083 additions and 19,182 deletions.
2 changes: 2 additions & 0 deletions lib/stripe-force/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module Constants
# application constants
POLL_FREQUENCY = T.let(3 * 60, Integer)
MAX_STRIPE_PRICE_PRECISION = 12
MAX_SALESFORCE_PRICE_PRECISION = 2
MAX_SF_RETRY_ATTEMPTS = 8

# Salesforce objects
Expand Down Expand Up @@ -274,6 +275,7 @@ class FeatureFlags < T::Enum
UPDATE_PRODUCT_ON_SYNC = new('update_product_on_sync')
SYNC_RECORD_FIELDS = new('sync_record_fields')
MDQ = new('mdq')
SALESFORCE_PRECISION = new('salesforce_precision')
end
end

Expand Down
34 changes: 28 additions & 6 deletions lib/stripe-force/translate/order/amendments.rb
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,10 @@ def self.contract_co_terminated?(mapper, contract_structure)
product_subscription_term: Integer,
billing_frequency: Integer,
days_prorating: Integer,
salesforce_precision: T::Boolean
).returns(BigDecimal)
end
def self.calculate_prorated_billing_amount(stripe_price:, subscription_term:, product_subscription_term:, billing_frequency:, days_prorating:)
def self.calculate_prorated_billing_amount(stripe_price:, subscription_term:, product_subscription_term:, billing_frequency:, days_prorating:, salesforce_precision: false)
# prorated billing amount is months of subscription term that is not included
prorated_subscription_term = subscription_term % billing_frequency
proration_percentage = BigDecimal(prorated_subscription_term) / BigDecimal(billing_frequency)
Expand All @@ -128,6 +129,9 @@ def self.calculate_prorated_billing_amount(stripe_price:, subscription_term:, pr

unit_amount_decimal = BigDecimal(stripe_price.unit_amount_decimal)
prorated_billing_amount = unit_amount_decimal * proration_percentage
if salesforce_precision
prorated_billing_amount = (prorated_billing_amount / 100).round(MAX_SALESFORCE_PRICE_PRECISION) * 100
end
prorated_billing_amount
end

Expand All @@ -152,10 +156,17 @@ def self.create_prorated_price_from_phase_item(mapper:, phase_item:, subscriptio
sf_order_item = phase_item.order_line
sf_order_amendment = user.sf_client.find(SF_ORDER, sf_order_item["OrderId"])
product_subscription_term = StripeForce::Utilities::SalesforceUtil.determine_quote_line_subscription_term(mapper, sf_order_item, sf_order_amendment)
prorated_billing_amount = calculate_prorated_billing_amount(stripe_price: stripe_price, subscription_term: subscription_term, product_subscription_term: product_subscription_term, billing_frequency: billing_frequency, days_prorating: days_prorating)
prorated_billing_amount = calculate_prorated_billing_amount(
stripe_price: stripe_price,
subscription_term: subscription_term,
product_subscription_term: product_subscription_term,
billing_frequency: billing_frequency,
days_prorating: days_prorating,
salesforce_precision: user.feature_enabled?(StripeForce::Constants::FeatureFlags::SALESFORCE_PRECISION))

# if the order is backdated and was synced after a billing cycle, we need to add the amount of the backdated billing cycle
if backdated_billing_cycles > 0
log.info 'adding backdated cycles to proration price', backdated_billing_cycles: backdated_billing_cycles
prorated_billing_amount += stripe_price.unit_amount_decimal.to_d * backdated_billing_cycles
end

Expand All @@ -181,13 +192,15 @@ def self.create_prorated_price_from_phase_item(mapper:, phase_item:, subscriptio

sig do
params(
user: StripeForce::User,
mapper: StripeForce::Mapper,
sf_order: Restforce::SObject,
phase_item: ContractItemStructure,
subscription_term: Integer,
billing_frequency: Integer
).returns(Hash)
end
def self.create_credit_price_data_from_terminated_phase_item(user:, phase_item:, subscription_term:, billing_frequency:)
def self.create_credit_price_data_from_terminated_phase_item(mapper:, sf_order:, phase_item:, subscription_term:, billing_frequency:)
user = mapper.user
# our goal here is to identify the correct amount to credit this user, pass it into a price_data hash to be bubbled to the Subscription Item

unless phase_item.fully_terminated?
Expand All @@ -204,10 +217,19 @@ def self.create_credit_price_data_from_terminated_phase_item(user:, phase_item:,
unused_term_percentage = 1
end

# we need to take into account the unused days (non anniversary amendment => day proration scenarios)
if user.feature_enabled?(StripeForce::Constants::FeatureFlags::NON_ANNIVERSARY_AMENDMENTS) && (user.feature_enabled?(StripeForce::Constants::FeatureFlags::DAY_PRORATIONS) || user.connector_settings[CONNECTOR_SETTING_CPQ_PRORATE_PRECISION] == 'month+day')
unused_term_percentage = StripeForce::Utilities::SalesforceUtil.calculate_price_multiplier(mapper, sf_order, phase_item.order_line, billing_frequency, is_terminated_line: true)
end

original_stripe_price = phase_item.price(user)
unit_amount_decimal = BigDecimal(original_stripe_price.unit_amount_decimal)
credit_amount = unit_amount_decimal * unused_term_percentage * -1

if user.feature_enabled?(StripeForce::Constants::FeatureFlags::SALESFORCE_PRECISION)
credit_amount = (credit_amount / 100).round(MAX_SALESFORCE_PRICE_PRECISION) * 100
end

# We should eventually use a Price here instead of price data. Details in ticket below
# https://jira.corp.stripe.com/browse/PLATINT-2090
price_data = {
Expand Down Expand Up @@ -275,9 +297,9 @@ def self.generate_proration_credits_from_terminated_phase_items(user:, mapper:,

# create price data, not price
price_data = create_credit_price_data_from_terminated_phase_item(
user: user,
mapper: mapper,
sf_order: sf_order_amendment,
phase_item: phase_item, # https://jira.corp.stripe.com/browse/PLATINT-2090

# TODO is there a better way to source these variables? They are required in a couple
subscription_term: subscription_term,
billing_frequency: billing_frequency
Expand Down
81 changes: 8 additions & 73 deletions lib/stripe-force/translate/price.rb
Original file line number Diff line number Diff line change
Expand Up @@ -150,10 +150,12 @@ def create_price_from_sf_object(sf_object, sf_product, stripe_product, sf_order_
subscription_term: subscription_term,
product_subscription_term: effective_subscription_term,
billing_frequency: billing_frequency,
days_prorating: 0
days_prorating: 0,
salesforce_precision: @user.feature_enabled?(FeatureFlags::SALESFORCE_PRECISION)
)

stripe_price.unit_amount_decimal = prorated_billing_amount.round(MAX_STRIPE_PRICE_PRECISION).to_s("F")

stripe_price.metadata[StripeForce::Translate::Metadata.metadata_key(@user, MetadataKeys::FRONTEND_PRORATION)] = true
end
end
Expand Down Expand Up @@ -342,7 +344,7 @@ def extract_tiered_price_params_from_order_line(sf_order_line)
def generate_price_params_from_sf_object(sf_object, sf_product)
# this should never happen, but provides self-documentation and extra test guards
if ![SF_ORDER_ITEM, SF_PRICEBOOK_ENTRY].include?(sf_object.sobject_type)
raise ArgumentError.new("price can only be created from an order line or pricebook entry")
raise ArgumentError.new("Price can only be created from an order item or pricebook entry.")
end

# TODO the tiered pricing logic should be extracted out to a separate method
Expand Down Expand Up @@ -449,82 +451,15 @@ def generate_price_params_from_sf_object(sf_object, sf_product)
log.info 'custom price not used, adjusting unit_amount_decimal', sf_order_item_id: sf_object.Id

sf_order = cache_service.get_record_from_cache(SF_ORDER, sf_object['OrderId'])

billing_frequency = StripeForce::Utilities::StripeUtil.billing_frequency_of_price_in_months(stripe_price)
price_multiplier = calculate_price_multiplier(mapper, sf_order, sf_object, billing_frequency)
price_multiplier = StripeForce::Utilities::SalesforceUtil.calculate_price_multiplier(mapper, sf_order, sf_object, billing_frequency)
stripe_price.unit_amount_decimal = T.cast(stripe_price.unit_amount_decimal, BigDecimal) / price_multiplier
end

stripe_price
end

sig { params(mapper: StripeForce::Mapper, sf_order: Restforce::SObject, sf_order_item: Restforce::SObject, billing_frequency: Integer).returns(BigDecimal) }
def calculate_price_multiplier(mapper, sf_order, sf_order_item, billing_frequency)
quote_subscription_term = StripeForce::Utilities::SalesforceUtil.extract_subscription_term_from_order!(mapper, sf_order)
effective_subscription_term = StripeForce::Utilities::SalesforceUtil.determine_quote_line_subscription_term(mapper, sf_order_item, sf_order)
cpq_price_multiplier = sf_order_item[CPQ_PRORATE_MULTIPLIER]

sf_order_end_date = StripeForce::Utilities::SalesforceUtil.extract_subscription_end_date_from_order(mapper, sf_order)
is_evergreen_order_item = sf_order_item[CPQ_PRODUCT_SUBSCRIPTION_TYPE] == CPQProductSubscriptionTypeOptions::EVERGREEN.serialize
if @user.feature_enabled?(FeatureFlags::NON_ANNIVERSARY_AMENDMENTS) && !sf_order_end_date.nil? && !is_evergreen_order_item
sf_order_start_date = StripeForce::Utilities::SalesforceUtil.extract_subscription_start_date_from_order(mapper, sf_order)

# calculate the number of days to prorate
days = StripeForce::Utilities::SalesforceUtil.calculate_days_to_prorate(
sf_order_start_date: sf_order_start_date,
sf_order_end_date: T.must(sf_order_end_date),
sf_order_subscription_term: quote_subscription_term)

# if there is a partial month due to a non-anniversary amendment
# we calculate the price multiplier differently depending on the CPQ Subscription Prorate Precision setting
if days > 0
if @user.feature_enabled?(FeatureFlags::DAY_PRORATIONS) || @user.connector_settings[CONNECTOR_SETTING_CPQ_PRORATE_PRECISION] == 'month+day'
# calculate the price multiplier for when CPQ Subscription Prorate Precision = 'Month + Day'
log.info 'using \'monthly + daily\' price multiplier calculations', sf_order_id: sf_order.Id, sf_order_item_id: sf_order_item.Id, days: days
calculated_price_multiplier = StripeForce::Utilities::SalesforceUtil.calculate_month_plus_day_price_multiplier(whole_months: quote_subscription_term, partial_month_days: days, product_subscription_term: effective_subscription_term)
else
# calculate the price multiplier for when CPQ Subscription Prorate Precision = 'Month'
# therefore cpq treats the partial month as a whole month so we add one to the provided subscription term
log.info 'using \'monthly\' price multiplier calculations', sf_order_id: sf_order.Id, sf_order_item_id: sf_order_item.Id
quote_subscription_term += 1
calculated_price_multiplier = BigDecimal(T.must(quote_subscription_term)) / BigDecimal(billing_frequency)
end

validate_price_multipliers(calculated_price_multiplier, cpq_price_multiplier, true)
return calculated_price_multiplier
if @user.feature_enabled?(StripeForce::Constants::FeatureFlags::SALESFORCE_PRECISION)
stripe_price.unit_amount_decimal = (stripe_price.unit_amount_decimal.to_d / 100).round(MAX_SALESFORCE_PRICE_PRECISION) * 100
end
end

# TODO should we adjust based on the quantity? Most likely, let's wait until tests fail
price_multiplier = BigDecimal(T.must(quote_subscription_term)) / BigDecimal(billing_frequency)

# TODO should test this further with proration amendments
# For MDQ orders, the quote subscription term is not the effective subscription term
if @user.feature_enabled?(FeatureFlags::MDQ) && !validate_price_multipliers(price_multiplier, cpq_price_multiplier, false)
log.info 'using effective subscription term instead of quote subscription term for mdq product'
price_multiplier = BigDecimal(T.must(effective_subscription_term)) / BigDecimal(billing_frequency)
end

price_multiplier
end

sig { params(calculated_price_multiplier: BigDecimal, cpq_price_multiplier: Float, throw_error: T.nilable(T::Boolean)).returns(T::Boolean) }
def validate_price_multipliers(calculated_price_multiplier, cpq_price_multiplier, throw_error)
# check that the calculated price multiplier is equal to the cpq provided price multiplier
# throw an error if they are not equal
# note: we do not use the cpq_price_multiplier since the prorate multiplier field that you see on CPQ objects is rounded
if !cpq_price_multiplier.nil?
cpq_price_multiplier = cpq_price_multiplier.to_d
threshold = 0.0000000001
if (calculated_price_multiplier - cpq_price_multiplier).abs > threshold
log.error 'calculated price multipler does not equal CPQ price multiplier', calculated_price_multiplier: calculated_price_multiplier, cpq_price_multiplier: cpq_price_multiplier
if throw_error
raise Integrations::Errors::TranslatorError.new("calculated price multiplier differs from cpq price multiplier")
end
return false
end
end

true
stripe_price
end
end
76 changes: 76 additions & 0 deletions lib/stripe-force/translate/utilities/salesforce_util.rb
Original file line number Diff line number Diff line change
Expand Up @@ -452,5 +452,81 @@ def self.is_renewal_order(cache_service, sf_order)
# for now, let's skip this to avoid another query
false
end

sig { params(mapper: StripeForce::Mapper, sf_order: Restforce::SObject, sf_order_item: Restforce::SObject, billing_frequency: Integer, is_terminated_line: T::Boolean).returns(BigDecimal) }
def self.calculate_price_multiplier(mapper, sf_order, sf_order_item, billing_frequency, is_terminated_line: false)
user = mapper.user
quote_subscription_term = StripeForce::Utilities::SalesforceUtil.extract_subscription_term_from_order!(mapper, sf_order)
effective_subscription_term = StripeForce::Utilities::SalesforceUtil.determine_quote_line_subscription_term(mapper, sf_order_item, sf_order)
cpq_price_multiplier = sf_order_item[CPQ_PRORATE_MULTIPLIER]

sf_order_end_date = StripeForce::Utilities::SalesforceUtil.extract_subscription_end_date_from_order(mapper, sf_order)
is_evergreen_order_item = sf_order_item[CPQ_PRODUCT_SUBSCRIPTION_TYPE] == CPQProductSubscriptionTypeOptions::EVERGREEN.serialize
if user.feature_enabled?(FeatureFlags::NON_ANNIVERSARY_AMENDMENTS) && !sf_order_end_date.nil? && !is_evergreen_order_item
sf_order_start_date = StripeForce::Utilities::SalesforceUtil.extract_subscription_start_date_from_order(mapper, sf_order)

# calculate the number of days to prorate
days = StripeForce::Utilities::SalesforceUtil.calculate_days_to_prorate(
sf_order_start_date: sf_order_start_date,
sf_order_end_date: T.must(sf_order_end_date),
sf_order_subscription_term: quote_subscription_term)

# if there is a partial month due to a non-anniversary amendment
# we calculate the price multiplier differently depending on the CPQ Subscription Prorate Precision setting
if days > 0
if user.feature_enabled?(FeatureFlags::DAY_PRORATIONS) || user.connector_settings[CONNECTOR_SETTING_CPQ_PRORATE_PRECISION] == 'month+day'
# calculate the price multiplier for when CPQ Subscription Prorate Precision = 'Month + Day'
log.info 'using \'monthly + daily\' price multiplier calculations', sf_order_id: sf_order.Id, sf_order_item_id: sf_order_item.Id, days: days
calculated_price_multiplier = StripeForce::Utilities::SalesforceUtil.calculate_month_plus_day_price_multiplier(whole_months: quote_subscription_term, partial_month_days: days, product_subscription_term: effective_subscription_term)
else
# calculate the price multiplier for when CPQ Subscription Prorate Precision = 'Month'
# therefore cpq treats the partial month as a whole month so we add one to the provided subscription term
log.info 'using \'monthly\' price multiplier calculations', sf_order_id: sf_order.Id, sf_order_item_id: sf_order_item.Id
quote_subscription_term += 1
calculated_price_multiplier = BigDecimal(T.must(quote_subscription_term)) / BigDecimal(billing_frequency)
end

if is_terminated_line
return calculated_price_multiplier
end

return validate_price_multipliers(calculated_price_multiplier, cpq_price_multiplier, true)
end
end

price_multiplier = BigDecimal(T.must(quote_subscription_term)) / BigDecimal(billing_frequency)
validated_price_multiplier = validate_price_multipliers(price_multiplier, cpq_price_multiplier, false)

# TODO should test this further with proration amendments
# For MDQ orders, the quote subscription term is not the effective subscription term
if user.feature_enabled?(FeatureFlags::MDQ) && price_multiplier != validated_price_multiplier
log.info 'using effective subscription term instead of quote subscription term for mdq product'
price_multiplier = validated_price_multiplier
end

price_multiplier
end

sig { params(calculated_price_multiplier: BigDecimal, cpq_price_multiplier: Float, throw_error: T.nilable(T::Boolean)).returns(BigDecimal) }
def self.validate_price_multipliers(calculated_price_multiplier, cpq_price_multiplier, throw_error)
# check that the calculated price multiplier is equal to the cpq provided price multiplier
# throw an error if they are not equal
# note: we do not use the cpq_price_multiplier since the prorate multiplier field that you see on CPQ objects is rounded
if !cpq_price_multiplier.nil?
cpq_price_multiplier = cpq_price_multiplier.to_d
if calculated_price_multiplier != cpq_price_multiplier
threshold = 0.0000000001
if (calculated_price_multiplier - cpq_price_multiplier).abs > threshold
log.error 'calculated price multipler is not within threshhold', calculated_price_multiplier: calculated_price_multiplier, cpq_price_multiplier: cpq_price_multiplier
if throw_error
raise Integrations::Errors::TranslatorError.new("Calculated price multiplier differs from cpq price multiplier.")
end
return cpq_price_multiplier
end
end
end

calculated_price_multiplier
end
end
end
Loading

0 comments on commit e0fed7e

Please sign in to comment.