diff --git a/lib/stripe-force/constants.rb b/lib/stripe-force/constants.rb index 1af19f0176..59d93bb6be 100644 --- a/lib/stripe-force/constants.rb +++ b/lib/stripe-force/constants.rb @@ -22,7 +22,9 @@ module Constants SF_CONTRACT = 'Contract' SF_STRIPE_COUPON = 'Stripe_Coupon_Beta__c' SF_STRIPE_COUPON_SERIALIZED = 'Stripe_Coupon_Beta_Serialized__c' + SF_STRIPE_COUPON_QUOTE_ASSOCIATION = 'Stripe_Coupon_Beta_Quote_Association__c' SF_STRIPE_COUPON_QUOTE_LINE_ASSOCIATION = 'Stripe_Coupon_Beta_Quote_Line_Associatio__c' + SF_STRIPE_COUPON_ORDER_ASSOCIATION = 'Stripe_Coupon_Beta_Order_Association__c' SF_STRIPE_COUPON_ORDER_ITEM_ASSOCIATION = 'Stripe_Coupon_Beta_Order_Item_Associatio__c' SF_ID = 'Id' @@ -184,6 +186,7 @@ class FeatureFlags < T::Enum CATCH_ALL_ERRORS = new('catch_all_errors') UPDATE_CUSTOMER_ON_ORDER_TRANSLATION = new('update_customer_on_order_creation') ACCOUNT_POLLING = new('account_polling') + COUPONS = new('coupons') end end diff --git a/lib/stripe-force/translate/coupon.rb b/lib/stripe-force/translate/coupon.rb index 6d8fd7b932..8bddfb4443 100644 --- a/lib/stripe-force/translate/coupon.rb +++ b/lib/stripe-force/translate/coupon.rb @@ -9,15 +9,15 @@ def translate_coupon(sf_coupon) catch_errors_with_salesforce_context(secondary: sf_coupon) do create_coupon_from_sf_coupon(sf_coupon) + ensure + locker.release_salesforce_record_lock(sf_coupon) end end def create_coupon_from_sf_coupon(sf_coupon) # check if the original sf coupon has been translated prior and exists in Stripe - # this is the coupon that the serialized coupon on the order/order item was copied from - - # TODO hook up to cache service - original_sf_coupon = sf.find(SF_STRIPE_COUPON, sf_coupon.Original_Stripe_Coupon_Beta_Id__c) + # this original coupon is the coupon that the order / order item coupon was copied from + original_sf_coupon = sf.find(prefixed_stripe_field(SF_STRIPE_COUPON), sf_coupon[prefixed_stripe_field("Original_Stripe_Coupon_Beta_Id__c").to_s]) existing_stripe_coupon = retrieve_from_stripe(Stripe::Coupon, original_sf_coupon) if existing_stripe_coupon existing_stripe_coupon = T.cast(existing_stripe_coupon, Stripe::Coupon) @@ -26,7 +26,7 @@ def create_coupon_from_sf_coupon(sf_coupon) # this should never happen unless the coupon data in Salesforce is mutated # if so, we want to create a new Stripe object and write back the new id if coupons_are_equal?(existing_stripe_coupon: existing_stripe_coupon, generated_stripe_coupon: generated_stripe_coupon) - log.info 'using existing stripe coupon', existing_stripe_coupon_id: existing_stripe_coupon.id + log.info 'reusing existing stripe coupon', existing_stripe_coupon_id: existing_stripe_coupon.id return existing_stripe_coupon end end @@ -60,39 +60,54 @@ def coupons_are_equal?(existing_stripe_coupon:, generated_stripe_coupon:) simple_field_check_passed end + # Coupon can be related to an order or an order item. Either way, Stripe expects these coupons to be specified as discounts: # https://site-admin.stripe.com/docs/api/subscription_schedules/object#subscription_schedule_object-phases-discounts - def discounts_from_sf_order_item(sf_order_item:) - sf_coupons = StripeForce::Translate.get_salesforce_stripe_coupons_associated_to_order_line(sf_client: @user.sf_client, sf_order_line_id: sf_order_item.Id) - if !sf_coupons || sf_coupons.empty? + def stripe_discounts_for_sf_object(sf_object:) + sf_coupons = get_salesforce_stripe_coupons_associated_to_sf_object(sf_client: @user.sf_client, sf_object: sf_object) + + if sf_coupons.nil? return end - log.info 'found coupons for sf order item', salesforce_object: sf_order_item + log.info 'found coupons for sf object', salesforce_object: sf_object - # this is the format the stripe api expects sf_coupons.map do |sf_coupon| coupon = translate_coupon(sf_coupon) {coupon: coupon.id} end end - def self.get_salesforce_stripe_coupons_associated_to_order_line(sf_client:, sf_order_line_id:) - order_item_associations = sf_client.query("Select Id from #{SF_STRIPE_COUPON_ORDER_ITEM_ASSOCIATION} where Order_Item__c = '#{sf_order_line_id}'") + def get_salesforce_stripe_coupons_associated_to_sf_object(sf_client:, sf_object:) + catch_errors_with_salesforce_context(secondary: sf_object) do + # coupons can either be related to an order or order item + if sf_object.sobject_type == SF_ORDER + association_obj_type = SF_STRIPE_COUPON_ORDER_ASSOCIATION + association_field = 'Order__c' + elsif sf_object.sobject_type == SF_ORDER_ITEM + association_obj_type = SF_STRIPE_COUPON_ORDER_ITEM_ASSOCIATION + association_field = 'Order_Item__c' + else + # this should never happen since coupons can only be tied to an order or order item + raise "unsupported sf object type for coupons #{sf_object.sobject_type}" + end - if !order_item_associations || order_item_associations.size == 0 - log.info "no stripe coupon order line associations related to this order line", salesforce_object: sf_order_line_id - return - end + associations = sf_client.query("Select Id from #{prefixed_stripe_field(association_obj_type)} where #{prefixed_stripe_field(association_field)} = '#{sf_object.Id}'") - # there could be multiple coupons associated with a single order line - coupons = order_item_associations.map do |order_item_association| - association = sf_client.find(SF_STRIPE_COUPON_ORDER_ITEM_ASSOCIATION, order_item_association.Id) - coupon = sf_client.query("Select Id from #{SF_STRIPE_COUPON_SERIALIZED} where Id = '#{association.Stripe_Coupon__c}'") + if !associations || associations.size == 0 + log.info "no stripe coupon associations related to this sf object", salesforce_object: sf_object + return + end - # return the coupon object - sf_client.find(SF_STRIPE_COUPON_SERIALIZED, coupon.first.Id) - end + # there could be multiple coupons associated with a single order line + coupons = associations.map do |association| + association = sf_client.find(association_obj_type, association.Id) + coupon = sf_client.query("Select Id from #{prefixed_stripe_field(SF_STRIPE_COUPON_SERIALIZED)} where Id = '#{association.Stripe_Coupon__c}'") - coupons + # return the coupon object + sf_client.find(prefixed_stripe_field(SF_STRIPE_COUPON_SERIALIZED), coupon.first.Id) + end + + coupons + end end end diff --git a/lib/stripe-force/translate/order.rb b/lib/stripe-force/translate/order.rb index 8a525db589..21c11d2238 100644 --- a/lib/stripe-force/translate/order.rb +++ b/lib/stripe-force/translate/order.rb @@ -199,6 +199,10 @@ def generate_phases_for_initial_order(sf_order:, invoice_items:, subscription_it metadata: Metadata.stripe_metadata_for_sf_object(@user, sf_order), } + if @user.feature_enabled?(FeatureFlags::COUPONS) + initial_phase["discounts"] = stripe_discounts_for_sf_object(sf_object: sf_order) + end + prorated_phase = nil # TODO this needs to be gated and synced with the specific flag that CF is using @@ -698,9 +702,12 @@ def phase_items_from_order_lines(sf_order_lines) phase_item = Stripe::SubscriptionItem.construct_from({ price: price.id, metadata: Metadata.stripe_metadata_for_sf_object(@user, sf_order_item), - discounts: discounts_from_sf_order_item(sf_order_item: sf_order_item), }) + if @user.feature_enabled?(FeatureFlags::COUPONS) + phase_item.discounts = stripe_discounts_for_sf_object(sf_object: sf_order_item) + end + phase_item_params = StripeForce::Utilities::SalesforceUtil.extract_salesforce_params!(mapper, sf_order_item, Stripe::SubscriptionItem) mapper.assign_values_from_hash(phase_item, phase_item_params) apply_mapping(phase_item, sf_order_item) diff --git a/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Order_Association__c/Stripe_Coupon_Beta_Order_Association__c.object-meta.xml b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Order_Association__c/Stripe_Coupon_Beta_Order_Association__c.object-meta.xml new file mode 100644 index 0000000000..efb520c53f --- /dev/null +++ b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Order_Association__c/Stripe_Coupon_Beta_Order_Association__c.object-meta.xml @@ -0,0 +1,174 @@ + + + + Accept + Default + + + Accept + Large + Default + + + Accept + Small + Default + + + CancelEdit + Default + + + CancelEdit + Large + Default + + + CancelEdit + Small + Default + + + Clone + Default + + + Clone + Large + Default + + + Clone + Small + Default + + + Delete + Default + + + Delete + Large + Default + + + Delete + Small + Default + + + Edit + Default + + + Edit + Large + Default + + + Edit + Small + Default + + + List + Default + + + List + Large + Default + + + List + Small + Default + + + New + Default + + + New + Large + Default + + + New + Small + Default + + + SaveEdit + Default + + + SaveEdit + Large + Default + + + SaveEdit + Small + Default + + + Tab + Default + + + Tab + Large + Default + + + Tab + Small + Default + + + View + Default + + + View + Large + Default + + + View + Small + Default + + false + SYSTEM + Deployed + Custom junction object linking a serialized Stripe Coupon and an Order Line. + false + true + false + false + false + false + false + true + true + ControlledByParent + + + {0000} + Quot + AutoNumber + + Stripe Coupon Beta Order + + ControlledByParent + + Order_Field_Must_Be_Set + Order__c is a required field. + true + ISBLANK(Order__c) + Order__c is a required field. + + Public + diff --git a/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Order_Association__c/fields/Order__c.field-meta.xml b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Order_Association__c/fields/Order__c.field-meta.xml new file mode 100644 index 0000000000..499055ed53 --- /dev/null +++ b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Order_Association__c/fields/Order__c.field-meta.xml @@ -0,0 +1,12 @@ + + + Order__c + false + + Order + Stripe Coupon Beta Order Connection + Stripe_Coupon_Beta_Order_Connection + false + false + Lookup + diff --git a/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Order_Association__c/fields/Stripe_Coupon__c.field-meta.xml b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Order_Association__c/fields/Stripe_Coupon__c.field-meta.xml new file mode 100644 index 0000000000..d0534659b1 --- /dev/null +++ b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Order_Association__c/fields/Stripe_Coupon__c.field-meta.xml @@ -0,0 +1,14 @@ + + + Stripe_Coupon__c + false + + Stripe_Coupon_Beta_Serialized__c + Stripe Coupon Beta Serialized Order Link + Stripe_Coupon_Beta_Serialized_Order_Link + 0 + false + false + MasterDetail + true + \ No newline at end of file diff --git a/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Quote_Association__c/Stripe_Coupon_Beta_Quote_Association__c.object-meta.xml b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Quote_Association__c/Stripe_Coupon_Beta_Quote_Association__c.object-meta.xml new file mode 100644 index 0000000000..f7e62307d0 --- /dev/null +++ b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Quote_Association__c/Stripe_Coupon_Beta_Quote_Association__c.object-meta.xml @@ -0,0 +1,167 @@ + + + + Accept + Default + + + Accept + Large + Default + + + Accept + Small + Default + + + CancelEdit + Default + + + CancelEdit + Large + Default + + + CancelEdit + Small + Default + + + Clone + Default + + + Clone + Large + Default + + + Clone + Small + Default + + + Delete + Default + + + Delete + Large + Default + + + Delete + Small + Default + + + Edit + Default + + + Edit + Large + Default + + + Edit + Small + Default + + + List + Default + + + List + Large + Default + + + List + Small + Default + + + New + Default + + + New + Large + Default + + + New + Small + Default + + + SaveEdit + Default + + + SaveEdit + Large + Default + + + SaveEdit + Small + Default + + + Tab + Default + + + Tab + Large + Default + + + Tab + Small + Default + + + View + Default + + + View + Large + Default + + + View + Small + Default + + false + SYSTEM + Deployed + Custom junction object linking a Stripe Coupon and a CPQ Quote. + false + true + false + false + false + false + false + true + true + ControlledByParent + + + {0000} + + AutoNumber + + Stripe Coupon Beta Quotes + + ControlledByParent + Public + diff --git a/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Quote_Association__c/fields/Quote__c.field-meta.xml b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Quote_Association__c/fields/Quote__c.field-meta.xml new file mode 100644 index 0000000000..641e621c19 --- /dev/null +++ b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Quote_Association__c/fields/Quote__c.field-meta.xml @@ -0,0 +1,14 @@ + + + Quote__c + false + + SBQQ__Quote__c + Stripe Coupon Beta Quote + Stripe_Coupon_Beta_Quote + 1 + false + false + MasterDetail + true + diff --git a/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Quote_Association__c/fields/Stripe_Coupon__c.field-meta.xml b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Quote_Association__c/fields/Stripe_Coupon__c.field-meta.xml new file mode 100644 index 0000000000..fce97a7a21 --- /dev/null +++ b/sfdx/force-app/main/default/objects/Stripe_Coupon_Beta_Quote_Association__c/fields/Stripe_Coupon__c.field-meta.xml @@ -0,0 +1,14 @@ + + + Stripe_Coupon__c + false + + Stripe_Coupon_Beta__c + Stripe Coupon Beta Quote + Stripe_Coupon_Beta_Quote + 0 + false + false + MasterDetail + true + diff --git a/sfdx/force-app/main/default/permissionsets/Stripe_Connector_Integration_User.permissionset-meta.xml b/sfdx/force-app/main/default/permissionsets/Stripe_Connector_Integration_User.permissionset-meta.xml index 34cde82048..430c9bcf1b 100644 --- a/sfdx/force-app/main/default/permissionsets/Stripe_Connector_Integration_User.permissionset-meta.xml +++ b/sfdx/force-app/main/default/permissionsets/Stripe_Connector_Integration_User.permissionset-meta.xml @@ -370,6 +370,11 @@ Stripe_Coupon_Beta_Order_Item_Associatio__c.Order_Item__c true + + true + Stripe_Coupon_Beta_Order_Association__c.Order__c + true + false diff --git a/sfdx/force-app/main/default/triggers/updateOrderCoupons.trigger b/sfdx/force-app/main/default/triggers/updateOrderCoupons.trigger new file mode 100644 index 0000000000..85ebf9c142 --- /dev/null +++ b/sfdx/force-app/main/default/triggers/updateOrderCoupons.trigger @@ -0,0 +1,67 @@ +trigger updateOrderCoupons on SBQQ__Quote__c (after update) { + try { + // for all newly ordered quotes, check if the quote has coupon and copy/duplicate to the corresponding order + for(SBQQ__Quote__c quote : Trigger.new) { + if (quote.SBQQ__Ordered__c == true && Trigger.oldMap.get(quote.Id).SBQQ__Ordered__c == false) { + // get the corresponding order for this quote + List order = [ + SELECT Id + FROM Order + WHERE SBQQ__Quote__c = :quote.Id + ]; + + if (order == null || order.size() == 0) + { + continue; + } + Id orderId = order.get(0).Id; + + // fetch the Stripe Coupon Quote Associations for this quote + List stripeCouponQuoteAssociations = [ + SELECT Id, Stripe_Coupon__c + FROM Stripe_Coupon_Beta_Quote_Association__c + WHERE Quote__c = :quote.Id + ]; + + if (stripeCouponQuoteAssociations == null) + { + continue; + } + + // for each Stripe Coupon Quote Association + for (Stripe_Coupon_Beta_Quote_Association__c stripeCouponQuoteAssociation: stripeCouponQuoteAssociations) + { + Stripe_Coupon_Beta__c quoteCoupon = [ + SELECT Amount_Off__c, Duration__c, Duration_In_Months__c, Max_Redemptions__c, Name__c, Percent_Off__c + FROM Stripe_Coupon_Beta__c + WHERE Id = :stripeCouponQuoteAssociation.Stripe_Coupon__c + ].get(0); + + // clone the Stripe Coupon on the quote, it will have a different Id + Stripe_Coupon_Beta_Serialized__c clonedCoupon = new Stripe_Coupon_Beta_Serialized__c( + Amount_Off__c = quoteCoupon.Amount_Off__c, + Duration__c = quoteCoupon.Duration__c, + Duration_In_Months__c = quoteCoupon.Duration_In_Months__c, + Max_Redemptions__c = quoteCoupon.Max_Redemptions__c, + Name__c = quoteCoupon.Name__c, + Percent_Off__c = quoteCoupon.Percent_Off__c, + Original_Stripe_Coupon_Beta_Id__c = quoteCoupon.Id + ); + // insert the cloned Stripe coupon + Database.insertImmediate((sObject)clonedCoupon); + + // create a Stripe Coupon Order Association junction object + Stripe_Coupon_Beta_Order_Association__c orderStripeCouponAssociation = new Stripe_Coupon_Beta_Order_Association__c( + Stripe_Coupon__c = clonedCoupon.Id, + Order__c = orderId + ); + + // insert this record + Database.insertImmediate((sObject)orderStripeCouponAssociation); + } + } + } + } catch (Exception e) { + errorLogger.create('updateOrderCouponsTrigger', e); + } +} \ No newline at end of file diff --git a/sfdx/force-app/main/default/triggers/updateOrderCoupons.trigger-meta.xml b/sfdx/force-app/main/default/triggers/updateOrderCoupons.trigger-meta.xml new file mode 100644 index 0000000000..f502e4bf01 --- /dev/null +++ b/sfdx/force-app/main/default/triggers/updateOrderCoupons.trigger-meta.xml @@ -0,0 +1,5 @@ + + + 54.0 + Active + diff --git a/test/integration/translate/test_coupon.rb b/test/integration/translate/test_coupon.rb index 6e4900712c..262b381449 100644 --- a/test/integration/translate/test_coupon.rb +++ b/test/integration/translate/test_coupon.rb @@ -6,6 +6,7 @@ class Critic::CouponTranslation < Critic::FunctionalTest before do @user = make_user(save: true) + @user.enable_feature(FeatureFlags::COUPONS) end it 'validation error thrown if SF Stripe coupon is created with both Amount_Off__c and Percent_Off__c set' do @@ -63,51 +64,6 @@ class Critic::CouponTranslation < Critic::FunctionalTest assert_equal(25, coupon_1.Percent_Off__c) end - it 'coupons are copied to order lines when a quote is ordered' do - # setup - PRODUCT_PRICE = 100 - sf_account_id = create_salesforce_account - sf_product_id, _sf_pricebook_id = salesforce_recurring_product_with_price(price: PRODUCT_PRICE) - - # create a SF CPQ quote - sf_quote_id = create_salesforce_quote(sf_account_id: sf_account_id, additional_quote_fields: { - CPQ_QUOTE_SUBSCRIPTION_START_DATE => now_time_formatted_for_salesforce, - CPQ_QUOTE_SUBSCRIPTION_TERM => TEST_DEFAULT_CONTRACT_TERM, - }) - - # create a quote with a product - quote_with_product = add_product_to_cpq_quote(sf_quote_id, sf_product_id: sf_product_id) - sf_quote_id = calculate_and_save_cpq_quote(quote_with_product) - - # retrieve the quote line - quote_lines = sf_get_related(sf_quote_id, CPQ_QUOTE_LINE) - assert_equal(1, quote_lines.size) - quote_line_id = quote_lines.first.Id - - # create a coupon and attach to the quote line - sf_stripe_coupon = create_salesforce_stripe_coupon(additional_fields: { - SalesforceStripeCouponFields::NAME => 'Special 50% off coupon', - SalesforceStripeCouponFields::PERCENT_OFF => 50, - }) - - # create the association object to map the coupon to the quote line - create_salesforce_stripe_coupon_quote_line_association(sf_quote_line_id: quote_line_id, sf_stripe_coupon_id: sf_stripe_coupon) - - sf_order = create_order_from_cpq_quote(sf_quote_id) - - # query for the association objects that have a reference to this order line - sf_order_line_items = sf_get_related(sf_order, SF_ORDER_ITEM) - assert_equal(1, sf_order_line_items.count) - - sf_order_item_id = sf_order_line_items.first.Id - associated_coupons = StripeForce::Translate.get_salesforce_stripe_coupons_associated_to_order_line(sf_client: @user.sf_client, sf_order_line_id: sf_order_item_id) - assert_equal(1, associated_coupons.size) - - order_line_coupon = associated_coupons.first - assert_equal('Special 50% off coupon', order_line_coupon.Name__c) - assert_equal(50, order_line_coupon.Percent_Off__c) - end - it 'translate sf order with multiple coupons on an order line' do # setup sf_account_id = create_salesforce_account @@ -185,11 +141,86 @@ class Critic::CouponTranslation < Critic::FunctionalTest assert_equal(stripe_percent_off_coupon.id, sf_percent_off_coupon[prefixed_stripe_field(GENERIC_STRIPE_ID)]) end - it 'translate an sf order with multiple coupons on order' do - # TODO need to add Quote/Order coupon association objects first + it 'translate an sf order with coupons on both the order and order items' do + # setup + sf_account_id = create_salesforce_account + sf_product_id, _sf_pricebook_id = salesforce_recurring_product_with_price + + # create a CPQ quote + sf_quote_id = create_salesforce_quote(sf_account_id: sf_account_id, additional_quote_fields: { + CPQ_QUOTE_SUBSCRIPTION_START_DATE => now_time_formatted_for_salesforce, + CPQ_QUOTE_SUBSCRIPTION_TERM => TEST_DEFAULT_CONTRACT_TERM, + }) + + # create a quote with a product + quote_with_product = add_product_to_cpq_quote(sf_quote_id, sf_product_id: sf_product_id) + sf_quote_id = calculate_and_save_cpq_quote(quote_with_product) + + # retrieve the quote line + quote_lines = sf_get_related(sf_quote_id, CPQ_QUOTE_LINE) + assert_equal(1, quote_lines.size) + quote_line_id = quote_lines.first.Id + + # create two coupons + sf_percent_off_coupon_id = create_salesforce_stripe_coupon(additional_fields: { + SalesforceStripeCouponFields::NAME => '25% off coupon', + SalesforceStripeCouponFields::PERCENT_OFF => 25, + }) + sf_amount_off_coupon_id = create_salesforce_stripe_coupon(additional_fields: { + SalesforceStripeCouponFields::NAME => '$50 off coupon', + SalesforceStripeCouponFields::AMOUNT_OFF => 50, + }) + + # create the quote line coupon association object to map the coupon to the quote line + create_salesforce_stripe_coupon_quote_line_association(sf_quote_line_id: quote_line_id, sf_stripe_coupon_id: sf_percent_off_coupon_id) + # create the quote coupon association object to map the coupon to the quote + create_salesforce_stripe_coupon_quote_association(sf_quote_id: sf_quote_id, sf_stripe_coupon_id: sf_amount_off_coupon_id) + + # create and translate the SF order + sf_order = create_order_from_cpq_quote(sf_quote_id) + StripeForce::Translate.perform_inline(@user, sf_order.Id) + + # fetch the stripe subscription schedule + sf_order.refresh + stripe_id = sf_order[prefixed_stripe_field(GENERIC_STRIPE_ID)] + subscription_schedule = Stripe::SubscriptionSchedule.retrieve(stripe_id, @user.stripe_credentials) + assert_equal(1, subscription_schedule.phases.count) + + # the first phase should have a coupon + first_phase = T.must(subscription_schedule.phases.first) + phase_discount = first_phase.discounts + assert_equal(1, phase_discount.count) + + # the first phase item should have one coupons + first_phase_item = T.must(first_phase.items.first) + phase_item_discount = first_phase_item.discounts + assert_equal(1, phase_item_discount.count) + + # retrieve the two stripe coupons + stripe_phase_coupon = Stripe::Coupon.retrieve(T.must(phase_discount.first).coupon, @user.stripe_credentials) + stripe_phase_item_coupon = Stripe::Coupon.retrieve(T.must(phase_item_discount.first).coupon, @user.stripe_credentials) + + # sanity check the stripe coupons have the right data + assert_equal(50, stripe_phase_coupon.amount_off) + assert_equal("usd", stripe_phase_coupon.currency) + assert_equal("once", stripe_phase_coupon.duration) + assert_equal(sf_amount_off_coupon_id, stripe_phase_coupon.metadata['salesforce_stripe_coupon_id']) + + assert_equal(25, stripe_phase_item_coupon.percent_off) + assert_equal("once", stripe_phase_item_coupon.duration) + assert_equal(sf_percent_off_coupon_id, stripe_phase_item_coupon.metadata['salesforce_stripe_coupon_id']) + + # fetch invoice and verify final amount due + sf_account = sf_get(sf_account_id) + stripe_customer_id = sf_account[prefixed_stripe_field(GENERIC_STRIPE_ID)] + invoice = Stripe::Invoice.list({customer: stripe_customer_id}, @user.stripe_credentials) + assert_equal(1, invoice.data.count) + + # expected price should equal $120 (original price) with 25%-off and $0-off coupons applied + assert_includes([(TEST_DEFAULT_PRICE * 0.75 - 50).to_i, ((TEST_DEFAULT_PRICE - 50) * 0.75).to_i], invoice.data.first.amount_due) end - it 'uses the same stripe coupon if translated twice' do + it 'reuses the same stripe coupon if translated twice' do # setup sf_account_id = create_salesforce_account sf_product_id_1, _sf_pricebook_id = salesforce_recurring_product_with_price @@ -274,7 +305,7 @@ class Critic::CouponTranslation < Critic::FunctionalTest StripeForce::Translate.perform_inline(@user, sf_order_1.Id) sf_order_1.refresh - # now update the coupon in salesforce and attempt to translate a new order using this coupon + # update the coupon in salesforce and attempt to translate a new order using this coupon sf.update!(SF_STRIPE_COUPON, { SF_ID => sf_percent_off_coupon_id, prefixed_stripe_field("Name__c") => 'Special coupon', @@ -312,15 +343,19 @@ class Critic::CouponTranslation < Critic::FunctionalTest phase_item_2 = T.must(first_phase_2.items.first) phase_item_2_discount = T.must(phase_item_2.discounts.first) - # the coupons on the phase items should not be equal + # the coupons on the phase items should be equal assert_not_equal(phase_item_1_discount.coupon, phase_item_2_discount.coupon) + + # confirm the stripe coupon name was updated + phase_item_1_stripe_coupon = Stripe::Coupon.retrieve(phase_item_1_discount.coupon, @user.stripe_credentials) + phase_item_2_stripe_coupon = Stripe::Coupon.retrieve(phase_item_2_discount.coupon, @user.stripe_credentials) + assert_not_equal(phase_item_1_stripe_coupon.name, phase_item_2_stripe_coupon.name) + assert_equal("55% off coupon", phase_item_1_stripe_coupon.name) + assert_equal("Special coupon", phase_item_2_stripe_coupon.name) end it 'stripe invoice final due amount reflects coupons' do - @user.enable_feature FeatureFlags::TEST_CLOCKS, update: true - # setup - order_start_date = now_time sf_account_id = create_salesforce_account sf_product_id, _sf_pricebook_id = salesforce_recurring_product_with_price @@ -357,15 +392,10 @@ class Critic::CouponTranslation < Critic::FunctionalTest sf_order = create_order_from_cpq_quote(sf_quote_id) StripeForce::Translate.perform_inline(@user, sf_order.Id) - # now let's advance the clock and pretend we are in the future to verify the invoice final due amount + # fetch invoice and verify final amount due sf_account = sf_get(sf_account_id) stripe_customer_id = sf_account[prefixed_stripe_field(GENERIC_STRIPE_ID)] - stripe_customer = stripe_get(stripe_customer_id) - refute_nil(stripe_customer.test_clock) - advance_test_clock(stripe_customer, (order_start_date + 1.day).to_i) - - # get invoice and verify final amount due - invoice = Stripe::Invoice.list({customer: stripe_customer.id}, @user.stripe_credentials) + invoice = Stripe::Invoice.list({customer: stripe_customer_id}, @user.stripe_credentials) assert_equal(1, invoice.data.count) # expected price should equal $120 with 25% off and $30 off coupons applied diff --git a/test/support/salesforce_factory.rb b/test/support/salesforce_factory.rb index 0ee17d200f..7e142a7642 100644 --- a/test/support/salesforce_factory.rb +++ b/test/support/salesforce_factory.rb @@ -120,6 +120,15 @@ def create_salesforce_product(additional_fields: {}) }.merge(additional_fields)) end + def create_salesforce_stripe_coupon_quote_association(sf_quote_id:, sf_stripe_coupon_id:) + sf_stripe_coupon_id ||= create_salesforce_stripe_coupon + + sf.create!(prefixed_stripe_field(SF_STRIPE_COUPON_QUOTE_ASSOCIATION), { + "Quote__c" => sf_quote_id, + "Stripe_Coupon__c" => sf_stripe_coupon_id, + }.transform_keys(&method(:prefixed_stripe_field))) + end + def create_salesforce_stripe_coupon_quote_line_association(sf_quote_line_id:, sf_stripe_coupon_id:) sf_stripe_coupon_id ||= create_salesforce_stripe_coupon