From 1bff76f3ca9636056def341d0797e2e9e53bf719 Mon Sep 17 00:00:00 2001 From: mbianco-stripe <45374579+mbianco-stripe@users.noreply.github.com> Date: Mon, 3 Oct 2022 13:28:13 -0700 Subject: [PATCH] Backend proration without amendment support (#818) * Initial backend proration test * Missing types * Split phases on initial order if it is backend prorated * Some documentation improvements * Helper for determining if initial order is prorated * Note about better helper to use * Unrelated script for queue management * Docs todo * Minor doc update * Extract out initial order phase generation * Check for one-time invoice before extracting subscription params * Fixing sync record link failure --- TODO | 2 + lib/stripe-force/translate/order.rb | 179 +++++++++++++----- .../translate/order/amendments.rb | 18 ++ lib/stripe-force/translate/order/helpers.rb | 26 +++ lib/stripe-force/translate/translate.rb | 2 +- scripts/remove_delayed_jobs.rb | 5 + sorbet/custom/stripe.rbi | 4 + .../integration/amendments/test_amendments.rb | 2 +- .../test_backend_prorated_amendments.rb | 94 ++++++++- .../amendments/test_proration_amendments.rb | 2 +- test/integration/test_order_failures.rb | 4 +- 11 files changed, 277 insertions(+), 61 deletions(-) create mode 100644 scripts/remove_delayed_jobs.rb diff --git a/TODO b/TODO index a59ea81d41..dd0118bc61 100644 --- a/TODO +++ b/TODO @@ -17,6 +17,8 @@ - Prices which are not created directly from pricebooks (i.e. from order lines, or duplicated because of the one-price-per-subscription) are archived (active = false) after they are used - Some good test clock docs https://groups.google.com/a/stripe.com/g/cloudflare/c/VjNW1Q4KD-0 - fyi if the filter is changed we don't retroactively pick up orders. the orders have to be updated in some fashion (just updating a note/description will do just fine) for it to be reprocessed +- On a backend prorated order amendment the same metadata is used as the initial order +- Tiered prices cannot be prorated Apex / checks: diff --git a/lib/stripe-force/translate/order.rb b/lib/stripe-force/translate/order.rb index 97e442523f..c7a7db8b50 100644 --- a/lib/stripe-force/translate/order.rb +++ b/lib/stripe-force/translate/order.rb @@ -85,13 +85,12 @@ def create_stripe_transaction_from_sf_order(sf_order) end sf_account = cache_service.get_record_from_cache(SF_ACCOUNT, sf_order[SF_ORDER_ACCOUNT]) - stripe_customer = translate_account(sf_account) sf_order_items = order_lines_from_order(sf_order) invoice_items, subscription_items = phase_items_from_order_lines(sf_order_items) - is_recurring_order = !subscription_items.empty? + is_recurring_order = !subscription_items.empty? if !is_recurring_order return create_stripe_invoice_from_order(stripe_customer, invoice_items, sf_order) end @@ -100,19 +99,92 @@ def create_stripe_transaction_from_sf_order(sf_order) subscription_params = StripeForce::Utilities::SalesforceUtil.extract_salesforce_params!(mapper, sf_order, Stripe::SubscriptionSchedule) - # TODO should file an API papercut for this + subscription_schedule = Stripe::SubscriptionSchedule.construct_from({ + # TODO this should be specified in the defaults hash... we should create a defaults hash https://jira.corp.stripe.com/browse/PLATINT-1501 + end_behavior: 'cancel', + metadata: Metadata.stripe_metadata_for_sf_object(@user, sf_order), + }) + + mapper.assign_values_from_hash(subscription_schedule, subscription_params) + apply_mapping(subscription_schedule, sf_order) + + subscription_schedule_phases = generate_phases_for_initial_order( + sf_order: sf_order, + invoice_items: invoice_items, + subscription_items: subscription_items, + subscription_schedule: subscription_schedule, + stripe_customer: stripe_customer + ) + + # TODO refactor once caching is stable, more notes in the `generate_phases_for_initial_rder` + if subscription_schedule_phases.is_a?(Stripe::Invoice) + return subscription_schedule_phases + end + + # TODO should file a Stripe API papercut for this, weird to have the start date on the header but the end date on the phase # when creating the subscription schedule the start_date must be specified on the heaer # when updating it, it is specified on the individual phase object - string_start_date_from_salesforce = subscription_params['start_date'] + + subscription_start_date_as_timestamp = StripeForce::Utilities::SalesforceUtil.salesforce_date_to_unix_timestamp(subscription_schedule.start_date) + subscription_schedule.start_date = subscription_start_date_as_timestamp + + Integrations::Utilities::StripeUtil.delete_field_from_stripe_object( + subscription_schedule, + :iterations + ) + + # TODO add mapping support against the subscription schedule phase + + subscription_schedule.customer = stripe_customer.id + subscription_schedule.phases = subscription_schedule_phases.compact + + # this should never happen, but we are still learning about CPQ + # if metered billing, quantity is not set, so we set to 1 + if OrderHelpers.extract_all_items_from_subscription_schedule(subscription_schedule).map {|l| l[:quantity] || 1 }.any?(&:zero?) + Integrations::ErrorContext.report_edge_case("quantity is zero on initial subscription schedule") + end + + # https://jira.corp.stripe.com/browse/PLATINT-1731 + days_until_due = subscription_schedule.[](:default_settings)&.[](:invoice_settings)&.[](:days_until_due) + if days_until_due + subscription_schedule.default_settings.invoice_settings.days_until_due = OrderHelpers.transform_payment_terms_to_days_until_due(days_until_due) + end + + subscription_schedule = Stripe::SubscriptionSchedule.create( + subscription_schedule.to_hash, + StripeForce::Utilities::StripeUtil.generate_idempotency_key_with_credentials(@user, sf_order) + ) + + log.info 'stripe subscription schedule created', stripe_subscription_schedule_id: subscription_schedule.id + + update_sf_stripe_id(sf_order, subscription_schedule) + + PriceHelpers.auto_archive_prices_on_subscription_schedule(@user, subscription_schedule) + + stripe_transaction + end + + # it is important that the subscription schedule is passed in before the start_date is transformed + sig do + params( + sf_order: T.untyped, + invoice_items: T::Array[ContractItemStructure], + subscription_items: T::Array[ContractItemStructure], + subscription_schedule: Stripe::SubscriptionSchedule, + stripe_customer: Stripe::Customer + ).returns(T.any(Stripe::Invoice, [Hash, T.nilable(Hash)])) + end + def generate_phases_for_initial_order(sf_order:, invoice_items:, subscription_items:, subscription_schedule:, stripe_customer:) + string_start_date_from_salesforce = subscription_schedule['start_date'] + # TODO should avoid using blind parse and instead enforce a strict SF datetime format + start_date_from_salesforce = DateTime.parse(string_start_date_from_salesforce) subscription_start_date_as_timestamp = StripeForce::Utilities::SalesforceUtil.salesforce_date_to_unix_timestamp(string_start_date_from_salesforce) - subscription_params['start_date'] = subscription_start_date_as_timestamp - subscription_term_from_sales_force = subscription_params.delete('iterations').to_i + subscription_term_from_sales_force = subscription_schedule['iterations'].to_i # originally `iterations` was used, but this fails when subscription term is less than a single billing cycle initial_phase_end_date = StripeForce::Utilities::SalesforceUtil.datetime_to_unix_timestamp( - DateTime.parse(string_start_date_from_salesforce) + - + subscription_term_from_sales_force.months + start_date_from_salesforce + subscription_term_from_sales_force.months ) # TODO we should have a check to ensure all quantities are positive @@ -122,10 +194,13 @@ def create_stripe_transaction_from_sf_order(sf_order) initial_phase = { add_invoice_items: invoice_items.map(&:stripe_params), items: subscription_items.map(&:stripe_params), + # TODO so annoying we cannot put the start_date here end_date: initial_phase_end_date, metadata: Metadata.stripe_metadata_for_sf_object(@user, sf_order), } + prorated_phase = nil + # TODO this needs to be gated and synced with the specific flag that CF is using if subscription_start_date_as_timestamp >= OrderAmendment.determine_current_time(@user, stripe_customer.id) # when `sub_sched_backdating_anchors_on_backdate` is enabled prorating the initial phase @@ -135,59 +210,54 @@ def create_stripe_transaction_from_sf_order(sf_order) initial_phase[:proration_behavior] = 'none' end - # TODO backend order prorations! - # is order prorated? we'll need a separate helper for this - # if so, iterate through phase items items and generate proration items - # create a new phase to store these items, this will be custom for the initial order - # we can release this, and then support amendments next - # pull this '2nd phase creation' out into a separate method so we can use it on the amendment side of things for amendments - # make sure the initial state on the order amendments includes the second phase - # we may need to do some weird phase merging because the initial order phase could be after the order amendment phase - - # TODO add mapping support against the subscription schedule phase + billing_frequency = OrderAmendment.calculate_billing_frequency_from_phase_items(@user, subscription_items) - # TODO subs in SF must always have an end date - stripe_transaction = Stripe::SubscriptionSchedule.construct_from({ - customer: stripe_customer.id, + is_initial_order_prorated = OrderHelpers.prorated_initial_order?( + phase_items: subscription_items, + subscription_term: subscription_term_from_sales_force, + billing_frequency: billing_frequency + ) - # TODO this should be specified in the defaults hash... we should create a defaults hash https://jira.corp.stripe.com/browse/PLATINT-1501 - end_behavior: 'cancel', + if is_initial_order_prorated + # create a new phase to store these items, this will be custom for the initial order + # pull this '2nd phase creation' out into a separate method so we can use it on the amendment side of things for amendments - # initial order will only ever contain a single phase - phases: [initial_phase], + # TODO next, we need to support prorated order amendments + # make sure the initial state on the order amendments includes the second phase + # we may need to do some weird phase merging because the initial order phase could be after the order amendment phase - metadata: Metadata.stripe_metadata_for_sf_object(@user, sf_order), - }) + invoice_items_for_prorations = OrderAmendment.generate_proration_items_from_phase_items( + user: @user, + sf_order_amendment: sf_order, + phase_items: subscription_items, + subscription_term: subscription_term_from_sales_force, + billing_frequency: billing_frequency + ) - mapper.assign_values_from_hash(stripe_transaction, subscription_params) - apply_mapping(stripe_transaction, sf_order) + backend_proration_term = subscription_term_from_sales_force % billing_frequency + backend_proration_start_date = start_date_from_salesforce + (subscription_term_from_sales_force - backend_proration_term).months + backend_proration_start_date_timestamp = StripeForce::Utilities::SalesforceUtil.datetime_to_unix_timestamp(backend_proration_start_date) - # this should never happen, but we are still learning about CPQ - # if metered billing, quantity is not set, so we set to 1 - if OrderHelpers.extract_all_items_from_subscription_schedule(stripe_transaction).map {|l| l[:quantity] || 1 }.any?(&:zero?) - Integrations::ErrorContext.report_edge_case("quantity is zero on initial subscription schedule") - end + prorated_phase = { + # we do not want to bill the user for a entire billing cycle, so we turn off prorations + proration_behavior: 'none', - # https://jira.corp.stripe.com/browse/PLATINT-1731 - days_until_due = stripe_transaction.[](:default_settings)&.[](:invoice_settings)&.[](:days_until_due) - if days_until_due - stripe_transaction.default_settings.invoice_settings.days_until_due = OrderHelpers.transform_payment_terms_to_days_until_due(days_until_due) - end + # in the prorated phase, the item list will be exactly the same as the initial phase + items: subscription_items.map(&:stripe_params), - stripe_transaction = Stripe::SubscriptionSchedule.create( - stripe_transaction.to_hash, - StripeForce::Utilities::StripeUtil.generate_idempotency_key_with_credentials(@user, sf_order) - ) + # on initial creation, you cannot specify a start_date! + end_date: initial_phase_end_date, - log.info 'stripe subscription or invoice created', stripe_resource_id: stripe_transaction.id + add_invoice_items: invoice_items_for_prorations, - update_sf_stripe_id(sf_order, stripe_transaction) + # TODO think about adding a special metadata field to indicate that this is a split phase + metadata: Metadata.stripe_metadata_for_sf_object(@user, sf_order), + } - if stripe_transaction.is_a?(Stripe::SubscriptionSchedule) - PriceHelpers.auto_archive_prices_on_subscription_schedule(@user, stripe_transaction) + initial_phase['end_date'] = backend_proration_start_date_timestamp end - stripe_transaction + [initial_phase, prorated_phase] end sig do @@ -272,6 +342,16 @@ def update_subscription_phases_from_order_amendments(contract_structure) sf_initial_order_items = order_lines_from_order(contract_structure.initial) _, aggregate_phase_items = phase_items_from_order_lines(sf_initial_order_items) + is_initial_order_prorated = OrderHelpers.prorated_initial_order?( + phase_items: aggregate_phase_items, + subscription_term: StripeForce::Utilities::SalesforceUtil.extract_subscription_term_from_order!(@mapper, contract_structure.initial), + billing_frequency: OrderAmendment.calculate_billing_frequency_from_phase_items(@user, aggregate_phase_items) + ) + + if is_initial_order_prorated + raise StripeForce::Errors::RawUserError.new("Amending prorated initial orders is not yet supported") + end + subscription_phases = subscription_schedule.phases # SF does not enforce mutation restrictions. It's possible to go in and modify anything you want in Salesforce @@ -320,8 +400,7 @@ def update_subscription_phases_from_order_amendments(contract_structure) # originally `iterations` was used, but this fails when subscription term is less than a single billing cycle phase_params['end_date'] = StripeForce::Utilities::SalesforceUtil.datetime_to_unix_timestamp( - DateTime.parse(string_start_date_from_salesforce) + - + subscription_term_from_sales_force.months + DateTime.parse(string_start_date_from_salesforce) + subscription_term_from_sales_force.months ) # TODO should we validate the end date vs the subscription schedule? diff --git a/lib/stripe-force/translate/order/amendments.rb b/lib/stripe-force/translate/order/amendments.rb index cb39efbaa5..95bbbd4eb5 100644 --- a/lib/stripe-force/translate/order/amendments.rb +++ b/lib/stripe-force/translate/order/amendments.rb @@ -25,6 +25,9 @@ def self.contract_co_terminated?(mapper, contract_structure) sig { params(mapper: StripeForce::Mapper, sf_order: Restforce::SObject).returns(Integer) } def self.calculate_order_end_date(mapper, sf_order) + # TODO should use a helper instead, which respects custom mapping + # salesforce_subscription_term = StripeForce::Utilities::SalesforceUtil.extract_subscription_term_from_order!(mapper, sf_order) + subscription_params = StripeForce::Utilities::SalesforceUtil.extract_salesforce_params!(mapper, sf_order, Stripe::SubscriptionSchedule) salesforce_start_date_as_string = subscription_params['start_date'] salesforce_subscription_term = subscription_params['iterations'].to_i @@ -34,6 +37,14 @@ def self.calculate_order_end_date(mapper, sf_order) end # NOTE at this point it's assumed that the price is NOT a metered billing item or tiered price + sig do + params( + user: StripeForce::User, + phase_item: ContractItemStructure, + subscription_term: Integer, + billing_frequency: Integer + ).returns(Stripe::Price) + end def self.create_prorated_price_from_phase_item(user:, phase_item:, subscription_term:, billing_frequency:) # at this point, we know what the billing amount is per billing cycle, since that has alread been defined # on the price object at the order line. We calculate the percentage of the original line price that should @@ -90,6 +101,8 @@ def self.generate_proration_items_from_phase_items(user:, sf_order_amendment:, p invoice_items_for_prorations = [] phase_items.each do |phase_item| + # metered prices are calculated based on user-provided usage information and therefore cannot be prorated because + # we do not know the full billing amount ahead of time. # TODO I don't like passing the user here, maybe pass in the user with the contract item? Going to see how ugly this feels... if PriceHelpers.metered_price?(phase_item.price(user)) log.info 'metered price, not prorating', @@ -98,6 +111,8 @@ def self.generate_proration_items_from_phase_items(user:, sf_order_amendment:, p next end + # right now, you can only have tiered pricing specified through consumption schedules, which are only used if the + # price is metered billed. We may need to support prorated tiered prices in the future. if PriceHelpers.tiered_price?(phase_item.price) log.info 'tiered price, not prorating', prorated_order_item_id: phase_item.order_line_id, @@ -105,6 +120,7 @@ def self.generate_proration_items_from_phase_items(user:, sf_order_amendment:, p next end + # if the price is not recurring, there is no need to prorate it since it will be fully billed up front if !PriceHelpers.recurring_price?(phase_item.price) log.info 'one time price, not prorating', prorated_order_item_id: phase_item.order_line_id, @@ -113,6 +129,8 @@ def self.generate_proration_items_from_phase_items(user:, sf_order_amendment:, p end # we only want to prorate the items that are unique to this order + # in other words, if the item needed to be prorated it would have already happened when processing the + # order that it was originally included on. if !phase_item.from_order?(sf_order_amendment) log.info 'line item not originated from this amendment, not prorating', prorated_order_item_id: phase_item.order_line_id, diff --git a/lib/stripe-force/translate/order/helpers.rb b/lib/stripe-force/translate/order/helpers.rb index 7f745c7be3..706d3d6b0c 100644 --- a/lib/stripe-force/translate/order/helpers.rb +++ b/lib/stripe-force/translate/order/helpers.rb @@ -9,6 +9,32 @@ module OrderHelpers include StripeForce::Constants extend SimpleStructuredLogger + sig do + params( + phase_items: T::Array[ContractItemStructure], + subscription_term: Integer, + billing_frequency: Integer, + ).returns(T::Boolean) + end + def self.prorated_initial_order?(phase_items:, subscription_term:, billing_frequency:) + log.info 'determining if initial order is prorated' + + if phase_items.empty? + log.info 'no subscription items, cannot be prorated order' + return false + end + + # if the subscription term does not match the billing frequency of the stripe item, then there will be some proration + if (subscription_term % billing_frequency) != 0 + log.info 'billing frequency is not divisible by subscription term, assuming prorated initial order', + subscription_term: subscription_term, + billing_frequency: billing_frequency + return true + end + + false + end + sig { params(subscription_schedule: Stripe::SubscriptionSchedule).returns(T::Array[T.any(Stripe::SubscriptionSchedulePhaseSubscriptionItem, Stripe::SubscriptionSchedulePhaseInvoiceItem)]) } def self.extract_all_items_from_subscription_schedule(subscription_schedule) subscription_schedule.phases.map(&:items).flatten + diff --git a/lib/stripe-force/translate/translate.rb b/lib/stripe-force/translate/translate.rb index 321a48cbab..332e260e3e 100644 --- a/lib/stripe-force/translate/translate.rb +++ b/lib/stripe-force/translate/translate.rb @@ -339,7 +339,7 @@ def construct_stripe_object(stripe_class:, salesforce_object:, additional_stripe stripe_object = stripe_class.construct_from(additional_stripe_params) - # the fields in the resulting hash could be dot-paths, so let's assign them using the mapper + # the keys (stripe references) in the resulting hash could be dot-paths, so let's assign them using the mapper mapper.assign_values_from_hash(stripe_object, stripe_fields) stripe_object.metadata = Metadata.stripe_metadata_for_sf_object(@user, salesforce_object) diff --git a/scripts/remove_delayed_jobs.rb b/scripts/remove_delayed_jobs.rb new file mode 100644 index 0000000000..3c93f2b82e --- /dev/null +++ b/scripts/remove_delayed_jobs.rb @@ -0,0 +1,5 @@ +Resque.remove_delayed_selection(SalesforceTranslateRecordJob) do |args| + puts args.inspect + username = args[0] + args.size == 5 +end \ No newline at end of file diff --git a/sorbet/custom/stripe.rbi b/sorbet/custom/stripe.rbi index cafcd8bb71..22200c4269 100644 --- a/sorbet/custom/stripe.rbi +++ b/sorbet/custom/stripe.rbi @@ -178,6 +178,10 @@ class Stripe::SubscriptionSchedulePhase < Stripe::StripeObject sig { params(arg: Integer).void } def start_date=(arg); end + sig { returns(String) } + def proration_behavior; end + + sig { returns(Integer)} def end_date; end diff --git a/test/integration/amendments/test_amendments.rb b/test/integration/amendments/test_amendments.rb index cfce605043..233737bec9 100644 --- a/test/integration/amendments/test_amendments.rb +++ b/test/integration/amendments/test_amendments.rb @@ -138,7 +138,7 @@ class Critic::OrderAmendmentTranslation < Critic::OrderAmendmentFunctionalTest # remove metered billing item completely amendment_data["lineItems"].first["record"][CPQ_QUOTE_QUANTITY] = 0 - amendment_data["record"][CPQ_QUOTE_SUBSCRIPTION_START_DATE] = amendment_start_date.strftime("%Y-%m-%d") + amendment_data["record"][CPQ_QUOTE_SUBSCRIPTION_START_DATE] = format_date_for_salesforce(amendment_start_date) amendment_data["record"][CPQ_QUOTE_SUBSCRIPTION_TERM] = amendment_term sf_quote_id = calculate_and_save_cpq_quote(amendment_data) diff --git a/test/integration/amendments/test_backend_prorated_amendments.rb b/test/integration/amendments/test_backend_prorated_amendments.rb index 860e5e421e..e96a6df566 100644 --- a/test/integration/amendments/test_backend_prorated_amendments.rb +++ b/test/integration/amendments/test_backend_prorated_amendments.rb @@ -8,28 +8,110 @@ class Critic::BackendProratedAmendmentTranslation < Critic::OrderAmendmentFuncti @user = make_user(save: true) end - it 'throws an error if the subscription term is not divisible by billing frequency and is greater than one' do - skip("this case will be handled in an upcoming release, right now this functionality is undefined") + it 'handles an initial order with a backend proration' do + backend_prorated_term = 6 + contract_term = TEST_DEFAULT_CONTRACT_TERM + backend_prorated_term + initial_start_date = now_time + end_date = initial_start_date + contract_term.months sf_product_id, sf_pricebook_entry_id = salesforce_recurring_product_with_price( additional_product_fields: { CPQ_QUOTE_BILLING_FREQUENCY => CPQBillingFrequencyOptions::ANNUAL.serialize, - CPQ_QUOTE_SUBSCRIPTION_TERM => 12, } ) sf_order = create_salesforce_order( sf_product_id: sf_product_id, additional_quote_fields: { - CPQ_QUOTE_SUBSCRIPTION_TERM => 13, + CPQ_QUOTE_SUBSCRIPTION_TERM => contract_term, CPQ_QUOTE_SUBSCRIPTION_START_DATE => now_time_formatted_for_salesforce, } ) - exception = assert_raises(Integrations::Errors::UserError) do + StripeForce::Translate.perform_inline(@user, sf_order.Id) + + sf_order.refresh + stripe_id = sf_order[prefixed_stripe_field(GENERIC_STRIPE_ID)] + subscription_schedule = Stripe::SubscriptionSchedule.retrieve(stripe_id, @user.stripe_credentials) + + # although there is just a single order, it is split into two pieces + assert_equal(2, subscription_schedule.phases.count) + + first_phase = T.must(subscription_schedule.phases.first) + second_phase = T.must(subscription_schedule.phases[1]) + + # second phase proration none is very important! + assert_equal("create_prorations", first_phase.proration_behavior) + assert_equal("none", second_phase.proration_behavior) + + # first phase should start now and end at the end of the billing cycle + assert_equal(0, first_phase.start_date - initial_start_date.to_i) + assert_equal(0, first_phase.end_date - (initial_start_date + TEST_DEFAULT_CONTRACT_TERM.months).to_i) + + # second phase should start at the end of the billing cycle and end at the contract term + assert_equal(0, second_phase.start_date - (initial_start_date + TEST_DEFAULT_CONTRACT_TERM.months).to_i) + assert_equal(0, second_phase.end_date - end_date.to_i) + + # first phase should have one item with no quantity, since it is a metered product + assert_equal(1, first_phase.items.count) + assert_equal(0, first_phase.add_invoice_items.count) + first_phase_item = T.must(first_phase.items.first) + assert_equal(1, first_phase_item[:quantity]) + + assert_equal(1, second_phase.items.count) + assert_equal(1, second_phase.add_invoice_items.count) + second_phase_item = T.must(second_phase.items.first) + prorated_phase_item = T.must(second_phase.add_invoice_items.first) + assert_equal(1, first_phase_item.quantity) + assert_equal(1, prorated_phase_item.quantity) + + second_item_price = Stripe::Price.retrieve(T.cast(second_phase_item.price, String), @user.stripe_credentials) + assert_equal(TEST_DEFAULT_PRICE, second_item_price.unit_amount_decimal.to_i) + assert_equal("month", second_item_price.recurring.interval) + assert_equal(12, second_item_price.recurring.interval_count) + + prorated_price = Stripe::Price.retrieve(T.cast(prorated_phase_item.price, String), @user.stripe_credentials) + assert_equal(TEST_DEFAULT_PRICE / 2, prorated_price.unit_amount_decimal.to_i) + assert_equal("one_time", prorated_price.type) + assert_equal("true", prorated_price.metadata[StripeForce::Translate::Metadata.metadata_key(@user, MetadataKeys::PRORATION)]) + end + + it 'handles a backend prorated order amendment' do + contract_term = 18 + backend_prorated_term = 6 + + amendment_term = 6 + initial_start_date = now_time + amendment_start_date = initial_start_date + (contract_term - amendment_term).months + amendment_end_date = amendment_start_date + amendment_term.months + + sf_product_id, sf_pricebook_entry_id = salesforce_recurring_product_with_price( + additional_product_fields: { + CPQ_QUOTE_BILLING_FREQUENCY => CPQBillingFrequencyOptions::ANNUAL.serialize, + } + ) + + sf_order = create_salesforce_order( + sf_product_id: sf_product_id, + additional_quote_fields: { + CPQ_QUOTE_SUBSCRIPTION_TERM => TEST_DEFAULT_CONTRACT_TERM + backend_prorated_term, + CPQ_QUOTE_SUBSCRIPTION_START_DATE => now_time_formatted_for_salesforce, + } + ) + + sf_contract = create_contract_from_order(sf_order) + amendment_data = create_quote_data_from_contract_amendment(sf_contract) + + amendment_data["lineItems"].first["record"][CPQ_QUOTE_QUANTITY] = 2 + amendment_data["record"][CPQ_QUOTE_SUBSCRIPTION_START_DATE] = format_date_for_salesforce(amendment_start_date) + amendment_data["record"][CPQ_QUOTE_SUBSCRIPTION_TERM] = amendment_term + sf_quote_id = calculate_and_save_cpq_quote(amendment_data) + sf_order_amendment = create_order_from_quote_data(amendment_data) + + exception = assert_raises(StripeForce::Errors::UserError) do StripeForce::Translate.perform_inline(@user, sf_order.Id) end - assert_match("Prorated order amendments are not yet supported", exception.message) + assert_match("Amending prorated initial orders is not yet supported", exception.message) end end diff --git a/test/integration/amendments/test_proration_amendments.rb b/test/integration/amendments/test_proration_amendments.rb index 1cf7b8d490..692b70c850 100644 --- a/test/integration/amendments/test_proration_amendments.rb +++ b/test/integration/amendments/test_proration_amendments.rb @@ -309,7 +309,7 @@ class Critic::ProratedAmendmentTranslation < Critic::OrderAmendmentFunctionalTes yearly_price = 120_00 yearly_price_2 = 150_00 - contract_term = 24 + contract_term = TEST_DEFAULT_CONTRACT_TERM * 2 amendment_term = 19 amendment_start_date = now_time + (contract_term - amendment_term).months amendment_end_date = amendment_start_date + amendment_term.months diff --git a/test/integration/test_order_failures.rb b/test/integration/test_order_failures.rb index 39053b83a3..556f110f30 100644 --- a/test/integration/test_order_failures.rb +++ b/test/integration/test_order_failures.rb @@ -76,8 +76,8 @@ class Critic::OrderFailureTest < Critic::FunctionalTest assert_match(sf_order.Id, sync_record[prefixed_stripe_field(SyncRecordFields::COMPOUND_ID.serialize)]) assert_match("The following required fields are missing from this Salesforce record: SBQQ__Quote__c.SBQQ__SubscriptionTerm__c", sync_record[prefixed_stripe_field(SyncRecordFields::RESOLUTION_MESSAGE.serialize)]) - assert_match(@user.salesforce_instance_url, sync_record[prefixed_stripe_field('Primary_Record__c')]) - assert_match(@user.salesforce_instance_url, sync_record[prefixed_stripe_field('Secondary_Record__c')]) + assert_match(sf_order.Id, sync_record[prefixed_stripe_field('Primary_Record__c')]) + assert_match(sf_order.Id, sync_record[prefixed_stripe_field('Secondary_Record__c')]) end it 'throws an error when a float is specified for a quantity' do