Skip to content

Commit

Permalink
Ensure updateCoupons trigger is deterministic (#988)
Browse files Browse the repository at this point in the history
  • Loading branch information
nadaismail-stripe authored Jan 25, 2023
1 parent 9361301 commit 1934b43
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 14 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
public with sharing class OrderCouponTriggerHandler {
public class CouponException extends Sentry_Exception {}

private Map<Id, Id> quoteIdsToOrderIds = null;
private Map<Id, Id> quoteIdsToOrderIds = null;
private Map<Id, SBQQ__QuoteLine__c> quoteLines = null;
// the order coupons that are bulk persisted at the end of the trigger execution
private List<Order_Stripe_Coupon__c> orderCouponsToSave = null;
Expand All @@ -21,6 +21,55 @@ public with sharing class OrderCouponTriggerHandler {
quoteLines = new Map<Id, SBQQ__QuoteLine__c>();
}

public void process() {
removePreviouslyProcessedOrders();
processQuoteCoupons();
processQuoteLineCoupons();
}

// in case the user has workflows that would cause the trigger to fire twice, let's ensure we don't create duplicate Order Stripe Coupons
public void removePreviouslyProcessedOrders() {
// create the reverse map of order id -> quote id
Map<Id, Id> orderIdsToQuoteIds = new Map<Id, Id>();
for (Id quoteId : quoteIdsToOrderIds.keySet()) {
orderIdsToQuoteIds.put(quoteIdsToOrderIds.get(quoteId), quoteId);
}

Map<Id, OrderItem> orderItemsById = new Map<Id, OrderItem>([SELECT Id, OrderId FROM OrderItem WHERE OrderId = :quoteIdsToOrderIds.values()]);

// query for all Order Stripe Coupons that are tied to these orders or order items
List<Order_Stripe_Coupon__c> coupons = [
SELECT
Order__c,
Order_Item__c
FROM Order_Stripe_Coupon__c
WHERE
Order__c = :quoteIdsToOrderIds.values()
OR Order_Item__c = :orderItemsById.keySet()
];

for (Order_Stripe_Coupon__c coupon : coupons) {
Id orderId = null;
if (coupon.Order_Item__c != null) {
orderId = orderItemsById.get(coupon.Order_Item__c).OrderId;
} else if (coupon.Order__c != null) {
orderId = coupon.Order__c;
}

// this should really never happen since an Order Stripe Coupon
// should always be created with either Order__c or Order_Item__c fields set
if (orderId == null) {
continue;
}

// remove any quote we have already processed
Id quoteId = orderIdsToQuoteIds.get(orderId);
if (quoteIdsToOrderIds.containsKey(quoteId)) {
quoteIdsToOrderIds.remove(quoteId);
}
}
}

public void processQuoteCoupons() {
// SECTION: fetch the quote->order coupon data
List<Quote_Stripe_Coupon_Association__c> qscAssociations = getQuoteCouponAssociations(quoteIdsToOrderIds.keySet());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ public with sharing class test_updateOrderCouponTrigger {
private static final Integer COUPON_PERCENT_OFF = 55;
private static final String COUPON_DURATION = 'once';
private static final String ORDER_STATUS_ACTIVATED = 'Activated';
private static final String ORDER_STATUS_DRAFT = 'Draft';

@IsTest
static public void testUpdateOrderCouponTriggerOnQuoteCoupons() {
Expand All @@ -13,15 +14,14 @@ public with sharing class test_updateOrderCouponTrigger {
return;
}

// Setup test data
// setup test data
SBQQ__Quote__c quote = createQuote();
Quote_Stripe_Coupon__c coupon = createStripeCoupon();
createStripeCouponQuoteAssociation(quote.Id, coupon.Id);

// fires trigger
Test.startTest();

Order order = activateOrderFromQuote(quote);

Test.stopTest();

// sanity check that the Quote Stripe Coupon Association was created
Expand All @@ -38,7 +38,7 @@ public with sharing class test_updateOrderCouponTrigger {
System.assertEquals(COUPON_DURATION, quoteCoupon.Duration__c);

// trigger should have created an Order Stripe Coupon
List<Order_Stripe_Coupon__c> orderStripeCoupons = getOrderStripeCoupon(quoteCoupon.Id);
List<Order_Stripe_Coupon__c> orderStripeCoupons = getOrderCouponFromQuoteCouponId(quoteCoupon.Id);
System.assertEquals(1, orderStripeCoupons.size());

Order_Stripe_Coupon__c orderStripeCoupon = orderStripeCoupons.get(0);
Expand All @@ -52,16 +52,15 @@ public with sharing class test_updateOrderCouponTrigger {
return;
}

// Setup test data
// setup test data
SBQQ__Quote__c quote = createQuote();
Quote_Stripe_Coupon__c coupon = createStripeCoupon();
SBQQ__QuoteLine__c quoteLine = [SELECT Id FROM SBQQ__QuoteLine__c WHERE SBQQ__Quote__c = :quote.Id];
createStripeCouponQuoteLineAssociation(quoteLine.Id, coupon.Id);

// fires trigger
Test.startTest();

Order order = activateOrderFromQuote(quote);

Test.stopTest();

List<SBQQ__QuoteLine__c> quoteLines = [SELECT Id FROM SBQQ__QuoteLine__c WHERE SBQQ__Quote__c = :quote.Id];
Expand All @@ -81,7 +80,7 @@ public with sharing class test_updateOrderCouponTrigger {
System.assertEquals(COUPON_DURATION, quoteCoupon.Duration__c);

// trigger should have created an Order Stripe Coupon
List<Order_Stripe_Coupon__c> orderStripeCoupons = getOrderStripeCoupon(quoteCoupon.Id);
List<Order_Stripe_Coupon__c> orderStripeCoupons = getOrderCouponFromQuoteCouponId(quoteCoupon.Id);
System.assertEquals(1, orderStripeCoupons.size());

Order_Stripe_Coupon__c orderStripeCoupon = orderStripeCoupons.get(0);
Expand All @@ -91,6 +90,48 @@ public with sharing class test_updateOrderCouponTrigger {
System.assertEquals(1, orderItems.size());
}

@IsTest
static public void testDoubleFiringTrigger() {
Boolean isCpqInstalled = utilities.isCpqEnabled();
if(!isCpqInstalled) {
return;
}

// setup test data
SBQQ__Quote__c quote = createQuote();
SBQQ__QuoteLine__c quoteLine = [SELECT Id FROM SBQQ__QuoteLine__c WHERE SBQQ__Quote__c = :quote.Id];

// add a coupon on the quote
Quote_Stripe_Coupon__c quoteCoupon = createStripeCoupon();
createStripeCouponQuoteAssociation(quote.Id, quoteCoupon.Id);

// add two coupons on the quote line
Quote_Stripe_Coupon__c quoteLineCoupon = createStripeCoupon();
createStripeCouponQuoteLineAssociation(quoteLine.Id, quoteLineCoupon.Id);
createStripeCouponQuoteLineAssociation(quoteLine.Id, quoteLineCoupon.Id);

// fire trigger by activating the quote
Order order = activateOrderFromQuote(quote);

// initial trigger should have created 3 order coupons (1 on the order and 2 on the order line)
List<Order_Stripe_Coupon__c> orderStripeCoupons = getOrderCouponFromQuoteCouponId(quoteCoupon.Id);
System.assertEquals(1, orderStripeCoupons.size());

List<Order_Stripe_Coupon__c> orderLineStripeCoupons = getOrderCouponFromQuoteCouponId(quoteLineCoupon.Id);
System.assertEquals(2, orderLineStripeCoupons.size());

// now let's refire the updateCoupons trigger
Test.startTest();
refireTrigger(order);
Test.stopTest();

// confirm that no new order coupons were created due to the trigger firing again
orderStripeCoupons = getOrderCouponFromQuoteCouponId(quoteCoupon.Id);
System.assertEquals(1, orderStripeCoupons.size());
orderLineStripeCoupons = getOrderCouponFromQuoteCouponId(quoteLineCoupon.Id);
System.assertEquals(2, orderLineStripeCoupons.size());
}

static private Order activateOrderFromQuote(SBQQ__Quote__c quote) {
// update quote to be ordered
quote.SBQQ__Ordered__c = true;
Expand Down Expand Up @@ -244,10 +285,19 @@ public with sharing class test_updateOrderCouponTrigger {
];
}

static public List<Order_Stripe_Coupon__c> getOrderStripeCoupon(Id quoteCouponId) {
static public List<Order_Stripe_Coupon__c> getOrderCouponFromQuoteCouponId(Id quoteCouponId) {
return [
SELECT Id, Name__c, Percent_Off__c, Duration__c, Quote_Stripe_Coupon_Id__c, Order__c
FROM Order_Stripe_Coupon__c
WHERE Quote_Stripe_Coupon_Id__c = :quoteCouponId];
}

// This helper is used only in test to cause the updateCoupons trigger to refire
static private void refireTrigger(Order order)
{
order.Status = ORDER_STATUS_DRAFT;
Database.update(order, true);
order.Status = ORDER_STATUS_ACTIVATED;
Database.update(order, true);
}
}
5 changes: 1 addition & 4 deletions sfdx/force-app/main/default/triggers/updateCoupons.trigger
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
trigger updateCoupons on Order (after update) {
public class CouponException extends Exception {}

try {
// for all new Orders, check if the corresponding Quote has coupons and duplicate/copy to the corresponding order
Map<Id, Id> quoteIdsToOrderIds = new Map<Id, Id>();
Expand All @@ -16,8 +14,7 @@ trigger updateCoupons on Order (after update) {
}

OrderCouponTriggerHandler handler = new OrderCouponTriggerHandler(quoteIdsToOrderIds);
handler.processQuoteCoupons();
handler.processQuoteLineCoupons();
handler.process();
handler.persistChanges();
handler.refreshState(new Map<Id, Id>());

Expand Down

0 comments on commit 1934b43

Please sign in to comment.