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