Skip to content

Commit

Permalink
Adding MultiCurrency Support to Coupons (#1010)
Browse files Browse the repository at this point in the history
* added coupon currency and test

* added failure test case

* wipe currency on non amount off coupons

* fixed coupon match

* added overrides

* added comment

* addressed PR feedback
  • Loading branch information
brennen-stripe authored Feb 23, 2023
1 parent d4a42a9 commit 0490b8f
Show file tree
Hide file tree
Showing 9 changed files with 255 additions and 12 deletions.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,12 @@ sfdx/bin/sfdx-wipe-account [email protected]
- Then use `sfdx force:org:create -s -f config/project-scratch-def.json -a cool-alias -d 30` to create a stratch org. Lasts for a max of 30d.
`alias sf=sfdx` is going to make your CLI-life easier.

- FOR MULTICURRENCY SCRATCH ORGS:
- Prior to running `generate-scratch-org`, do the following:
- Go to `config/project-scratch-def.json` and set `enableMultiCurrency` to true
- Go to `sfdx/force-app/main/scratchSetup/layouts/PricebookEntry-Custom Price Book Entry Layout.layout-meta.xml` and make sure the `CurrencyIsoCode` layout item is uncommented.
- After running `generate-scratch-org` and following it's manual set up, go to `/lightning/setup/CompanyCurrency/home` and set up `GBP` as a new currency (exchange rate doesn't matter).

Apex triggers run synchronously, TODO add to notes

## Finding an existing package
Expand Down
1 change: 1 addition & 0 deletions lib/stripe-force/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ class SalesforceStripeCouponFields < T::Enum
DURATION = new('Duration__c')
DURATION_IN_MONTHS = new('Duration_In_Months__c')
MAX_REDEMPTIONS = new('Max_Redemptions__c')
CURRENCY_ISO_CODE = new(StripeForce::Constants::SF_CURRENCY_ISO_CODE)
end
end

Expand Down
9 changes: 5 additions & 4 deletions lib/stripe-force/translate/coupon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,12 @@ def coupons_are_equal?(existing_stripe_coupon:, generated_stripe_coupon:)
end

def sanitize_stripe_coupon(stripe_coupon)
# Stripe expects amount_off to be specified in cents
if !stripe_coupon["amount_off"].nil?
stripe_coupon["currency"] = 'usd'
if stripe_coupon["amount_off"].nil?
# Non-amount_off coupons should not have a currency set (auto-set in construct_stripe_object)
stripe_coupon["currency"] = nil
else
# Stripe expects amount_off to be specified in cents
stripe_coupon["amount_off"] = normalize_float_amount_for_stripe(stripe_coupon.currency, stripe_coupon["amount_off"].to_s, @user)

end

# Prevents Stripe API error 'Stripe::InvalidRequestError: Invalid integer: 1.0'
Expand Down
5 changes: 5 additions & 0 deletions lib/stripe-force/translate/translate.rb
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,11 @@ def construct_stripe_object(stripe_class:, salesforce_object:, additional_stripe

stripe_object.metadata = Metadata.stripe_metadata_for_sf_object(@user, salesforce_object)

# Prices and Coupons require currency fields
if [Stripe::Price, Stripe::Coupon].include?(stripe_object.class)
stripe_object['currency'] = Integrations::Utilities::Currency.currency_for_sf_object(@user, salesforce_object)
end

apply_mapping(stripe_object, salesforce_object)

sanitize(stripe_object)
Expand Down
5 changes: 3 additions & 2 deletions sfdx/config/project-scratch-def.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@

{
"orgName": "[email protected]",
"adminEmail": "[email protected]",
"edition": "Developer",
"settings": {
"currencySettings":{
"enableMultiCurrency": true
"currencySettings":{
"enableMultiCurrency": false
},
"mobileSettings": {
"enableS1EncryptedStoragePref2": false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
<behavior>Required</behavior>
<field>UnitPrice</field>
</layoutItems>
<!-- <layoutItems>
<behavior>Readonly</behavior>
<field>CurrencyIsoCode</field>
</layoutItems> -->
<layoutItems>
<behavior>Edit</behavior>
<field>UseStandardPrice</field>
Expand Down
203 changes: 203 additions & 0 deletions test/integration/test_multicurrency.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ class Critic::OrderTranslation < Critic::FunctionalTest
before do
@user = make_user(save: true)
# Only run on Multi-Currency Enabled CI Accounts
# If you are running this locally on a multi-currency scratch org, please go into make_user
# and add your scratch org ID similar to brennen's
unless @user.is_multicurrency_org?
skip("Skipping multicurrency test on non-multicurrency org")
end
Expand Down Expand Up @@ -200,6 +202,158 @@ class Critic::OrderTranslation < Critic::FunctionalTest
assert_equal(SF_ORDER, sync_records.first[prefixed_stripe_field(SyncRecordFields::PRIMARY_OBJECT_TYPE.serialize)])
assert_equal(SyncRecordResolutionStatuses::SUCCESS.serialize, sync_records.first[prefixed_stripe_field(SyncRecordFields::RESOLUTION_STATUS.serialize)])
end

it 'translates an order with a coupon in a currency other than USD' do
@user.enable_feature(FeatureFlags::COUPONS)

# add a custom metadata field mapping
@user.field_mappings['coupon'] = {
'metadata.sf_coupon_mapped_metadata_field' => prefixed_stripe_field('Name__c'),
'metadata.sf_order_mapped_metadata_field' => prefixed_stripe_field('Order_Item__c.OrderId'),
}
@user.save

currency_iso_code = 'GBP'

# setup
sf_account_id = create_salesforce_account
sf_product_id, _sf_pricebook_id = salesforce_recurring_product_with_price(currency_iso_code: currency_iso_code)

# create a CPQ quote
sf_quote_id = create_salesforce_quote(sf_account_id: sf_account_id, currency_iso_code: currency_iso_code, 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

sf_amount_off_coupon_id = create_salesforce_stripe_coupon(additional_fields: {
SalesforceStripeCouponFields::NAME => '£10 off coupon',
SalesforceStripeCouponFields::AMOUNT_OFF => 10,
SalesforceStripeCouponFields::CURRENCY_ISO_CODE => currency_iso_code,
})

# create the quote line coupon association object to map the coupons to the quote line
create_salesforce_stripe_coupon_quote_line_association(sf_quote_line_id: quote_line_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)

# first phase should have one item
first_phase = T.must(subscription_schedule.phases.first)
assert_equal(1, first_phase.items.count)

# the first phase item should have two coupons
first_phase_item = T.must(first_phase.items.first)
discounts = first_phase_item.discounts
assert_equal(1, discounts.count)

# retrieve the two stripe coupons
stripe_coupon = Stripe::Coupon.retrieve(T.must(discounts.first).coupon, @user.stripe_credentials)

# sanity check both stripe coupons have the right data
assert_equal(10 * 100, stripe_coupon.amount_off)
assert_equal(currency_iso_code.downcase, stripe_coupon.currency)
assert_equal("once", stripe_coupon.duration)

sf_amount_off_order_coupon = get_sf_order_coupon_from_quote_coupon_id(sf_amount_off_coupon_id)
assert_equal(sf_amount_off_order_coupon.Id, stripe_coupon.metadata['salesforce_order_stripe_coupon_id'])
assert_equal('£10 off coupon', stripe_coupon.metadata['sf_coupon_mapped_metadata_field'])
assert_equal(sf_order.Id, stripe_coupon.metadata['sf_order_mapped_metadata_field'])

# confirm the stripe coupon ids were written back to the quote coupons in salesforce
sf_amount_off_coupon = sf_get(sf_amount_off_coupon_id)
assert_equal(stripe_coupon.id, sf_amount_off_coupon[prefixed_stripe_field(GENERIC_STRIPE_ID)])
end

it 'allows association of a percent off coupon with an quote in a different currency' do
@user.enable_feature(FeatureFlags::COUPONS)

# add a custom metadata field mapping
@user.field_mappings['coupon'] = {
'metadata.sf_coupon_mapped_metadata_field' => prefixed_stripe_field('Name__c'),
'metadata.sf_order_mapped_metadata_field' => prefixed_stripe_field('Order_Item__c.OrderId'),
}
@user.save

quote_currency_iso_code = 'GBP'
coupon_currency_iso_code = 'USD'

# setup
sf_account_id = create_salesforce_account
sf_product_id, _sf_pricebook_id = salesforce_recurring_product_with_price(currency_iso_code: quote_currency_iso_code)

# create a CPQ quote
sf_quote_id = create_salesforce_quote(sf_account_id: sf_account_id, currency_iso_code: quote_currency_iso_code, 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

sf_percent_off_coupon_id = create_salesforce_stripe_coupon(additional_fields: {
SalesforceStripeCouponFields::NAME => '20% off coupon',
SalesforceStripeCouponFields::PERCENT_OFF => 20,
SalesforceStripeCouponFields::CURRENCY_ISO_CODE => coupon_currency_iso_code,
})

# create the quote line coupon association object to map the coupons 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 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)

# first phase should have one item
first_phase = T.must(subscription_schedule.phases.first)
assert_equal(1, first_phase.items.count)

# the first phase item should have two coupons
first_phase_item = T.must(first_phase.items.first)
discounts = first_phase_item.discounts
assert_equal(1, discounts.count)

# retrieve the two stripe coupons
stripe_coupon = Stripe::Coupon.retrieve(T.must(discounts.first).coupon, @user.stripe_credentials)

assert_equal(20, stripe_coupon.percent_off)
assert_equal("once", stripe_coupon.duration)

sf_percent_off_order_coupon = get_sf_order_coupon_from_quote_coupon_id(sf_percent_off_coupon_id)
assert_equal(sf_percent_off_order_coupon.Id, stripe_coupon.metadata['salesforce_order_stripe_coupon_id'])
assert_equal('20% off coupon', stripe_coupon.metadata['sf_coupon_mapped_metadata_field'])
assert_equal(sf_order.Id, stripe_coupon.metadata['sf_order_mapped_metadata_field'])

sf_percent_off_coupon = sf_get(sf_percent_off_coupon_id)
assert_equal(stripe_coupon.id, sf_percent_off_coupon[prefixed_stripe_field(GENERIC_STRIPE_ID)])
end
end

describe 'failure cases' do
Expand Down Expand Up @@ -256,6 +410,55 @@ class Critic::OrderTranslation < Critic::FunctionalTest
assert_equal(SF_ORDER, sync_records.first[prefixed_stripe_field(SyncRecordFields::PRIMARY_OBJECT_TYPE.serialize)])
assert_equal(SyncRecordResolutionStatuses::ERROR.serialize, sync_records.first[prefixed_stripe_field(SyncRecordFields::RESOLUTION_STATUS.serialize)])
end

it 'fails to create associate a coupon with an quote in a different currency' do
@user.enable_feature(FeatureFlags::COUPONS)

# add a custom metadata field mapping
@user.field_mappings['coupon'] = {
'metadata.sf_coupon_mapped_metadata_field' => prefixed_stripe_field('Name__c'),
'metadata.sf_order_mapped_metadata_field' => prefixed_stripe_field('Order_Item__c.OrderId'),
}
@user.save

quote_currency_iso_code = 'GBP'
coupon_currency_iso_code = 'USD'

# setup
sf_account_id = create_salesforce_account
sf_product_id, _sf_pricebook_id = salesforce_recurring_product_with_price(currency_iso_code: quote_currency_iso_code)

# create a CPQ quote
sf_quote_id = create_salesforce_quote(sf_account_id: sf_account_id, currency_iso_code: quote_currency_iso_code, 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

sf_amount_off_coupon_id = create_salesforce_stripe_coupon(additional_fields: {
SalesforceStripeCouponFields::NAME => '£10 off coupon',
SalesforceStripeCouponFields::AMOUNT_OFF => 10,
SalesforceStripeCouponFields::CURRENCY_ISO_CODE => coupon_currency_iso_code,
})

exception = assert_raises(Restforce::ErrorCode::FieldCustomValidationException) do
# create the quote line coupon association object to map the coupons to the quote line
create_salesforce_stripe_coupon_quote_line_association(sf_quote_line_id: quote_line_id, sf_stripe_coupon_id: sf_amount_off_coupon_id)
end

assert_equal(
"FIELD_CUSTOM_VALIDATION_EXCEPTION: The Currency of the Quote (GBP) does not match the Currency of the Quote Stripe Coupon (USD).",
exception.message
)
end
end

end
13 changes: 12 additions & 1 deletion test/support/common_helpers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,21 @@ def make_user(sandbox: false, save: false, random_user_id: false, livemode: fals
user.connector_settings[CONNECTOR_SETTING_SALESFORCE_NAMESPACE] = SalesforceNamespaceOptions::NONE.serialize
end

# MULTI_CURRENCY ORG OVERRIDES -------------------------------------------------------------------------
# If you are adding an account here, make sure you have added 'GBP' in Setup -> Manage Currencies

# Default to False
user.connector_settings[CONNECTOR_SETTING_MULTICURRENCY_ENABLED] = false

# [email protected]
if user.salesforce_account_id == "00D5f000006O9HAEA0"
user.connector_settings[CONNECTOR_SETTING_MULTICURRENCY_ENABLED] = true
end
# brennen-multi-curr-scratch
if user.salesforce_account_id == "00DDM000003rZgW2AU"
if user.salesforce_account_id == "00D52000000YKbHEAW"
user.connector_settings[CONNECTOR_SETTING_MULTICURRENCY_ENABLED] = true
end
# ------------------------------------------------------------------------------------------------------

# clocks won't be enabled in prod, so we want to mimic this
user.disable_feature(FeatureFlags::TEST_CLOCKS)
Expand Down
21 changes: 16 additions & 5 deletions test/support/salesforce_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ def create_salesforce_opportunity(sf_account_id:, currency_iso_code: nil, additi
currency_iso_code ||= @user.connector_settings['default_currency']

if @user.is_multicurrency_org?
additional_fields.merge({
SF_CURRENCY_ISO_CODE => currency_iso_code,
additional_fields = additional_fields.merge({
StripeForce::Constants::SF_CURRENCY_ISO_CODE => currency_iso_code,
})
end

Expand All @@ -100,8 +100,8 @@ def create_salesforce_price(sf_product_id: nil, price: nil, currency_iso_code: n
currency_iso_code ||= @user.connector_settings['default_currency']

if @user.is_multicurrency_org?
additional_fields.merge({
SF_CURRENCY_ISO_CODE => currency_iso_code,
additional_fields = additional_fields.merge({
StripeForce::Constants::SF_CURRENCY_ISO_CODE => currency_iso_code,
})
end

Expand Down Expand Up @@ -146,10 +146,21 @@ def create_salesforce_stripe_coupon_quote_association(sf_quote_id:, sf_stripe_co
def create_salesforce_stripe_coupon_quote_line_association(sf_quote_line_id:, sf_stripe_coupon_id:)
sf_stripe_coupon_id ||= create_salesforce_stripe_coupon

# Our association objects unfortunately have currency fields in multicurrency orgs, so we must pass
# the coupon's currency to the association here.
# This is error checked in the UI with this PR: https://github.com/stripe/stripe-salesforce/pull/1006
currency_field = {}
if @user.is_multicurrency_org?
sf_stripe_coupon = sf_get(sf_stripe_coupon_id)
currency_field = {
StripeForce::Constants::SF_CURRENCY_ISO_CODE => sf_stripe_coupon[StripeForce::Constants::SF_CURRENCY_ISO_CODE],
}
end

sf.create!(prefixed_stripe_field(QUOTE_LINE_SF_STRIPE_COUPON_ASSOCIATION), {
"Quote_Line__c" => sf_quote_line_id,
QUOTE_SF_STRIPE_COUPON => sf_stripe_coupon_id,
}.transform_keys(&method(:prefixed_stripe_field)))
}.merge(currency_field).transform_keys(&method(:prefixed_stripe_field)))
end

def create_salesforce_stripe_coupon(additional_fields: {})
Expand Down

0 comments on commit 0490b8f

Please sign in to comment.