Skip to content

Commit

Permalink
#1003 - Multi-Currency Coupons (probably v1) (#1006)
Browse files Browse the repository at this point in the history
* Initial changes

* abstracted the query logic for reuse in tests and to reduce duplication

* tests passing in non-multicurr org now

* Cleaned up some magic strings, and finished the test for multi-currency code

* 97% code coverage, woot

* Rename the classes something better

* Fix broken tests

* Fix error in non-multi-curr orgs

* Added trigger class to permission set

* Made percent off coupons ignored for validation, added tests around it

* fix tests in multi-curr enabled but single curr envs

* Clean up testing changes

* put back spaces to remove any changes from the file
  • Loading branch information
jmather-c authored Feb 22, 2023
1 parent 609a48b commit a7c9fec
Show file tree
Hide file tree
Showing 14 changed files with 984 additions and 59 deletions.
92 changes: 60 additions & 32 deletions sfdx/force-app/main/default/classes/OrderCouponTriggerHandler.cls
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ public with sharing class OrderCouponTriggerHandler {
// the order coupons that are bulk persisted at the end of the trigger execution
private List<Order_Stripe_Coupon__c> orderCouponsToSave = null;

private static final String FIELD_CURRENCY_ISO_CODE = constants.FIELD_CURRENCY_ISO_CODE;

public OrderCouponTriggerHandler(Map<Id, Id> quoteIdsToOrderIds) {
this.refreshState(quoteIdsToOrderIds);
}
Expand Down Expand Up @@ -151,7 +153,7 @@ public with sharing class OrderCouponTriggerHandler {

public static Order_Stripe_Coupon__c createOrderCoupon(Quote_Stripe_Coupon__c quoteCoupon) {
// clone the Quote Coupon on the quote, it will have a different Id
return new Order_Stripe_Coupon__c(
Order_Stripe_Coupon__c osc = new Order_Stripe_Coupon__c(
Amount_Off__c = quoteCoupon.Amount_Off__c,
Duration__c = quoteCoupon.Duration__c,
Duration_In_Months__c = quoteCoupon.Duration_In_Months__c,
Expand All @@ -160,23 +162,39 @@ public with sharing class OrderCouponTriggerHandler {
Percent_Off__c = quoteCoupon.Percent_Off__c,
Quote_Stripe_Coupon_Id__c = quoteCoupon.Id
);

if (UserInfo.isMultiCurrencyOrganization()) {
osc.put(FIELD_CURRENCY_ISO_CODE, quoteCoupon.get(FIELD_CURRENCY_ISO_CODE));
}

return osc;
}

private List<Quote_Stripe_Coupon_Association__c> getQuoteCouponAssociations(Set<Id> quoteIds) {
return new List<Quote_Stripe_Coupon_Association__c>([
SELECT
Quote__c,
Quote_Stripe_Coupon__c,
Quote_Stripe_Coupon__r.Id,
Quote_Stripe_Coupon__r.Amount_Off__c,
Quote_Stripe_Coupon__r.Duration__c,
Quote_Stripe_Coupon__r.Duration_In_Months__c,
Quote_Stripe_Coupon__r.Max_Redemptions__c,
Quote_Stripe_Coupon__r.Name__c,
Quote_Stripe_Coupon__r.Percent_Off__c
FROM Quote_Stripe_Coupon_Association__c
WHERE Quote__c IN :quoteIds
]);
List<String> fields = new List<String> {
'Quote__c',
'Quote_Stripe_Coupon__c',
'Quote_Stripe_Coupon__r.Id',
'Quote_Stripe_Coupon__r.Amount_Off__c',
'Quote_Stripe_Coupon__r.Duration__c',
'Quote_Stripe_Coupon__r.Duration_In_Months__c',
'Quote_Stripe_Coupon__r.Max_Redemptions__c',
'Quote_Stripe_Coupon__r.Name__c',
'Quote_Stripe_Coupon__r.Percent_Off__c'
};

List<String> multiCurrFields = new List<String> {
'Quote_Stripe_Coupon__r.' + FIELD_CURRENCY_ISO_CODE,
FIELD_CURRENCY_ISO_CODE
};

return utilities.execMultiCurrLookupQuery(
Quote_Stripe_Coupon_Association__c.SObjectType,
'Quote__c',
quoteIds,
fields,
multiCurrFields
);
}

private Map<Id, SBQQ__QuoteLine__c> getQuoteLinesByIds(Set<Id> quoteIds) {
Expand All @@ -197,23 +215,33 @@ public with sharing class OrderCouponTriggerHandler {
}

private List<Quote_Line_Stripe_Coupon_Association__c> getQuoteLineCouponAssociations(Set<Id> quoteLineIds) {
return [
SELECT
Id,
Quote_Line__c,
Quote_Line__r.Id,
Quote_Line__r.SBQQ__Quote__c,
Quote_Stripe_Coupon__c,
Quote_Stripe_Coupon__r.Id,
Quote_Stripe_Coupon__r.Amount_Off__c,
Quote_Stripe_Coupon__r.Duration__c,
Quote_Stripe_Coupon__r.Duration_In_Months__c,
Quote_Stripe_Coupon__r.Max_Redemptions__c,
Quote_Stripe_Coupon__r.Name__c,
Quote_Stripe_Coupon__r.Percent_Off__c
FROM Quote_Line_Stripe_Coupon_Association__c
WHERE Quote_Line__c IN :quoteLineIds
];
List<String> fields = new List<String> {
'Id',
'Quote_Line__c',
'Quote_Line__r.Id',
'Quote_Line__r.SBQQ__Quote__c',
'Quote_Stripe_Coupon__c',
'Quote_Stripe_Coupon__r.Id',
'Quote_Stripe_Coupon__r.Amount_Off__c',
'Quote_Stripe_Coupon__r.Duration__c',
'Quote_Stripe_Coupon__r.Duration_In_Months__c',
'Quote_Stripe_Coupon__r.Max_Redemptions__c',
'Quote_Stripe_Coupon__r.Name__c',
'Quote_Stripe_Coupon__r.Percent_Off__c'
};

List<String> multiCurrFields = new List<String> {
'Quote_Stripe_Coupon__r.' + FIELD_CURRENCY_ISO_CODE,
FIELD_CURRENCY_ISO_CODE
};

return utilities.execMultiCurrLookupQuery(
Quote_Line_Stripe_Coupon_Association__c.SObjectType,
'Quote_Line__c',
quoteLineIds,
fields,
multiCurrFields
);
}

private Map<Id, Id> getOrderItemIdByQuoteLineIds(Set<Id> quoteLineIds) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
/**
* Created by jmather-c on 2/17/23.
*/

public with sharing class QuoteCouponAssociationTriggerHandler {
// allows us to bypass the trigger in tests
@TestVisible
private static Boolean TRIGGER_ENABLED = true;

// left non-static and non-final so we can tweak and test in non-multi-curr envs
@TestVisible
private String ASSOC_FIELD_CURRENCY_ISO_CODE = constants.FIELD_CURRENCY_ISO_CODE;
@TestVisible
private String QUOTE_FIELD_CURRENCY_ISO_CODE = constants.FIELD_CURRENCY_ISO_CODE;
@TestVisible
private String COUPON_FIELD_CURRENCY_ISO_CODE = constants.FIELD_CURRENCY_ISO_CODE;
@TestVisible
private static final String LABEL_QUOTE = SBQQ__Quote__c.SObjectType.getDescribe().getLabel();
@TestVisible
private static final String LABEL_QUOTE_COUPON = Quote_Stripe_Coupon__c.SObjectType.getDescribe().getLabel();
@TestVisible
private static final String LABEL_QUOTE_STRIPE_COUPON_ASSOC = Quote_Stripe_Coupon_Association__c.SObjectType.getDescribe().getLabel();
@TestVisible
private static final String LABEL_QUOTE_LINE_STRIPE_COUPON_ASSOC = Quote_Line_Stripe_Coupon_Association__c.SObjectType.getDescribe().getLabel();
@TestVisible
private static final String FORMAT_DEPENDENT_ERROR_MESSAGE = 'The Currency of the {0} ({1}) does not match the Currency of the {2} ({3}).';
private static final String FORMAT_ASSOC_ERROR_MESSAGE = 'The Currency of the {0} ({1}) does not match the Currency of the {2} ({3}) or the {4} ({5}).';

public void process(List<Quote_Stripe_Coupon_Association__c> quoteAssocs) {
if (TRIGGER_ENABLED == false) {
return;
}

Set<Id> couponIds = pickIds(quoteAssocs, Quote_Stripe_Coupon_Association__c.Quote_Stripe_Coupon__c);
Set<Id> quoteIds = pickIds(quoteAssocs, Quote_Stripe_Coupon_Association__c.Quote__c);

Map<Id, String> couponCurrencies = getCouponIsoCodeMap(couponIds);
Map<Id, String> quoteCurrencies = getQuoteIsoCodeMap(quoteIds);

for (Quote_Stripe_Coupon_Association__c assoc : quoteAssocs) {
String couponCurrency = couponCurrencies.get(assoc.Quote_Stripe_Coupon__c);
String quoteCurrency = quoteCurrencies.get(assoc.Quote__c);
String assocCurrency = (String) assoc.get(ASSOC_FIELD_CURRENCY_ISO_CODE);

// this would only be null if the coupon did not end up being queried because it is not an Amount Off coupon
if (couponCurrency == null) {
continue;
}

validateCurrency(assocCurrency, quoteCurrency, couponCurrency, assoc, LABEL_QUOTE_STRIPE_COUPON_ASSOC);
}
}

public void process(List<Quote_Line_Stripe_Coupon_Association__c> quoteLineAssocs) {
if (TRIGGER_ENABLED == false) {
return;
}

Set<Id> couponIds = pickIds(quoteLineAssocs, Quote_Line_Stripe_Coupon_Association__c.Quote_Stripe_Coupon__c);
Set<Id> quoteLineIds = pickIds(quoteLineAssocs, Quote_Line_Stripe_Coupon_Association__c.Quote_Line__c);

Map<Id, String> couponCurrencies = getCouponIsoCodeMap(couponIds);
Map<Id, String> quoteLineCurrencies = getQuoteLineIsoCodeMap(quoteLineIds);

for (Quote_Line_Stripe_Coupon_Association__c assoc : quoteLineAssocs) {
String couponCurrency = couponCurrencies.get(assoc.Quote_Stripe_Coupon__c);
String quoteCurrency = quoteLineCurrencies.get(assoc.Quote_Line__c);
String assocCurrency = (String) assoc.get(ASSOC_FIELD_CURRENCY_ISO_CODE);

// this would only be null if the coupon did not end up being queried because it is not an Amount Off coupon
if (couponCurrency == null) {
continue;
}

validateCurrency(assocCurrency, quoteCurrency, couponCurrency, assoc, LABEL_QUOTE_LINE_STRIPE_COUPON_ASSOC);
}
}

@TestVisible
private void validateCurrency(String assocCurrency, String quoteCurrency, String couponCurrency, SObject obj, String objLabel) {
Boolean check1 = assocCurrency == couponCurrency;
Boolean check2 = assocCurrency == quoteCurrency;
Boolean check3 = couponCurrency == quoteCurrency;

System.debug('Checks: ' + check1 + ', ' + check2 + ', ' + check3);

if (check1 && check2 && check3) {
return;
}

if (check1 == false && check2 == false) {
String errorMsg = String.format(FORMAT_ASSOC_ERROR_MESSAGE,
new String[] { objLabel, assocCurrency, LABEL_QUOTE, quoteCurrency, LABEL_QUOTE_COUPON, couponCurrency});
obj.addError(ASSOC_FIELD_CURRENCY_ISO_CODE, errorMsg);
}

if (check3 == false) {
String errorMsg = String.format(FORMAT_DEPENDENT_ERROR_MESSAGE,
new String[] { LABEL_QUOTE, quoteCurrency, LABEL_QUOTE_COUPON, couponCurrency });
obj.addError(errorMsg);
}
}

@TestVisible
private Map<Id, String> getCouponIsoCodeMap(Set<Id> couponIds) {
String ids = utilities.idsToQueryString(couponIds);

String query = 'SELECT Id, ' + COUPON_FIELD_CURRENCY_ISO_CODE + ' FROM Quote_Stripe_Coupon__c WHERE Id IN ' + ids;
// make the validation not apply to % off coupons.
query += ' AND Amount_Off__c > 0';
List<SObject> coupons = Database.query(query);

Map<Id, String> result = new Map<Id, String>();

for (SObject coupon : coupons) {
result.put(coupon.Id, (String) coupon.get(COUPON_FIELD_CURRENCY_ISO_CODE));
}

return result;
}

@TestVisible
private Map<Id, String> getQuoteIsoCodeMap(Set<Id> quoteIds) {
String ids = utilities.idsToQueryString(quoteIds);
String query = 'SELECT Id, ' + QUOTE_FIELD_CURRENCY_ISO_CODE + ' FROM SBQQ__Quote__c WHERE Id IN ' + ids;
List<SObject> quotes = Database.query(query);

Map<Id, String> result = new Map<Id, String>();

for (SObject quote : quotes) {
result.put(quote.Id, (String) quote.get(QUOTE_FIELD_CURRENCY_ISO_CODE));
}

return result;
}

@TestVisible
private Map<Id, String> getQuoteLineIsoCodeMap(Set<Id> quoteLineIds) {
String ids = utilities.idsToQueryString(quoteLineIds);
String query = 'SELECT Id, SBQQ__Quote__r.' + QUOTE_FIELD_CURRENCY_ISO_CODE + ' FROM SBQQ__QuoteLine__c WHERE Id IN ' + ids;
List<SObject> quoteLines = Database.query(query);

Map<Id, String> result = new Map<Id, String>();

for (SObject quoteLine : quoteLines) {
SObject quote = quoteLine.getSObject('SBQQ__Quote__r');
result.put(quoteLine.Id, (String) quote.get(QUOTE_FIELD_CURRENCY_ISO_CODE));
}

return result;
}

@TestVisible
private Set<Id> pickIds(List<SObject> objs, SObjectField field) {
return pickIds(objs, field.getDescribe().getName());
}

private Set<Id> pickIds(List<SObject> objs, String field) {
Set<Id> result = new Set<Id>();

for (SObject obj : objs) {
result.add((String) obj.get(field));
}

return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
<apiVersion>54.0</apiVersion>
<status>Active</status>
</ApexClass>
2 changes: 2 additions & 0 deletions sfdx/force-app/main/default/classes/constants.cls
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ public with sharing class constants {
public static FINAL String SETUP_DATA_RECORD_NAME = 'SetupData';
public static FINAL String PACKAGED_PERMISSION_SET_NAME = 'Stripe Connector Integration User';

public static final String FIELD_CURRENCY_ISO_CODE = 'CurrencyIsoCode';

// this seemingly-useless code is used for the bootstrap process in the setup.page
public String getNamespace() {
return constants.NAMESPACE;
Expand Down
Loading

0 comments on commit a7c9fec

Please sign in to comment.