From d121d33f422a1c8498707293b960967ad120e8e8 Mon Sep 17 00:00:00 2001 From: Alejandro Molina Date: Mon, 15 Dec 2025 13:22:47 -0700 Subject: [PATCH 1/4] [FUN-16399] Ensure conflict resolution options are checked before applying Constant Mappings too --- src/classes/frDonationTest.cls | 392 +++++++++++++++++---------------- src/classes/frModel.cls | 39 ++-- 2 files changed, 231 insertions(+), 200 deletions(-) diff --git a/src/classes/frDonationTest.cls b/src/classes/frDonationTest.cls index 388f0e2..70417a9 100644 --- a/src/classes/frDonationTest.cls +++ b/src/classes/frDonationTest.cls @@ -39,10 +39,8 @@ */ @isTest public class frDonationTest { - - // - // syncEntity_newDonor - // + + static testMethod void syncEntity_newDonor() { if (frUtil.hasNPCobjects()) { return; // skip in NPC org @@ -51,19 +49,19 @@ public class frDonationTest { createMapping('name', 'Description'); createMapping('amount', 'amount'); createMapping('donation_cretime', 'CloseDate'); - + insert new frMapping__c(Name = 'Test String', Is_Constant__c = true, Constant_Value__c = 'Closed Won', sf_Name__c = 'StageName', Type__c = frDonation.TYPE); insert new frMapping__c(Name = 'Test Percent', Is_Constant__c = true, Constant_Value__c = '95', sf_Name__c = 'Probability', Type__c = frDonation.TYPE); insert new frMapping__c(Name = 'Test Double', Is_Constant__c = true, Constant_Value__c = '1.5', sf_Name__c = 'totalopportunityquantity', Type__c = frDonation.TYPE); - + frTestUtil.createTestPost(getTestRequest()); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity newOpportunity = [ SELECT Id, fr_ID__c, StageName, Name, Description, Probability, TotalOpportunityQuantity @@ -73,12 +71,12 @@ public class frDonationTest { System.assertEquals('Closed Won', newOpportunity.StageName, 'The constant mapping for StageName was not used'); System.assertEquals(95, newOpportunity.Probability, 'The constant mapping for Probability was not used'); System.assertEquals(1.5, newOpportunity.TotalOpportunityQuantity, 'The constant mapping for Total Opportunity Quantity was not used'); - + String expectedNameAndDesc = String.valueOf(getTestRequest().get('name')); System.assertEquals(expectedNameAndDesc, newOpportunity.Name, 'The mapping for name should have been applied'); System.assertEquals(expectedNameAndDesc, newOpportunity.Description, 'The mapping for desc should have been applied'); } - + static testMethod void syncEntity_newDonor_NPC() { if (!frUtil.hasNPCobjects()) { return; // skip in non-NPC org @@ -93,12 +91,12 @@ public class frDonationTest { insert new frMapping__c(Name = 'Test Double', Is_Constant__c = true, Constant_Value__c = '1.5', sf_Name__c = 'totalopportunityquantity', Type__c = frDonation.TYPE); insert frSetupController.getGiftTransactionDefaults(); - + frTestUtil.createTestPost(getTestRequest()); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); List gt = Database.query( 'SELECT Id, Name, Description '+ //, StageName__c, Probability__c, TotalOpportunityQuantity__c ' + @@ -108,15 +106,12 @@ public class frDonationTest { //System.assertEquals('Closed Won', (String)gt[0].get('StageName__c')); //System.assertEquals(95, (Decimal)gt[0].get('Probability__c')); //System.assertEquals(1.5, (Decimal)gt[0].get('TotalOpportunityQuantity__c')); - + String expectedNameAndDesc = String.valueOf(getTestRequest().get('name')); System.assertEquals(expectedNameAndDesc, (String)gt[0].get('Name')); System.assertEquals(expectedNameAndDesc, (String)gt[0].get('Description')); } - - // - // syncEntity_existingDonor - // + static testMethod void syncEntity_existingDonor() { if (frUtil.hasNPCobjects()) { return; @@ -124,13 +119,13 @@ public class frDonationTest { Contact testContact = frDonorTest.getTestContact(); createMapping('name', 'Name'); createMapping('donation_cretime', 'CloseDate'); - + frTestUtil.createTestPost(getTestRequest()); - + Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity newOpportunity = [ @@ -141,7 +136,7 @@ public class frDonationTest { System.assertEquals(testContact.Id, newOpportunity.fr_Donor__c, 'The funraise sf donor id was not populated to the opportunity\'s contact lookup field'); } - + static testMethod void syncEntity_existingDonor_NPC() { if (!frUtil.hasNPCobjects()) { return; @@ -150,15 +145,15 @@ public class frDonationTest { Account testAccount = frDonorTest.getTestAccount(true); insert frSetupController.getGiftTransactionDefaults(); createMapping('name', 'Name'); - + Map request = getTestRequest(); request.put('donorId', '856'); frTestUtil.createTestPost(request); - + Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); frTestUtil.assertNoErrors(); List gt = Database.query( @@ -166,10 +161,8 @@ public class frDonationTest { ); System.assertEquals(testAccount.Id, gt[0].get('DonorId')); } - - // - // syncEntity_CampaignDonation - // + + static testMethod void syncEntity_CampaignDonation() { if (frUtil.hasNPCobjects()) { return; @@ -177,20 +170,20 @@ public class frDonationTest { createMapping('name', 'Name'); createMapping('donation_cretime', 'CloseDate'); Contact testContact = frDonorTest.getTestContact(); - + Campaign testCampaign = new Campaign(fr_ID__c = '10', Name = 'Test Campaign', ExpectedRevenue = 123, Description = 'Test', Status = 'Published'); insert testCampaign; - + Map request = getTestRequest(); request.put('campaignGoalId', 10); request.put('campaignMappingDisabled', false); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity newOpportunity = [ @@ -203,7 +196,7 @@ public class frDonationTest { System.assertEquals(testCampaign.Id, newOpportunity.CampaignId, 'The campaign Id was not added to the donation'); } - + static testMethod void syncEntity_CampaignDonation_NPC() { if (!frUtil.hasNPCobjects()) { return; @@ -215,17 +208,17 @@ public class frDonationTest { Campaign testCampaign = new Campaign(fr_ID__c = '10', Name = 'Test Campaign', ExpectedRevenue = 123, Description = 'Test', Status = 'Published'); insert testCampaign; - + Map req = getTestRequest(); req.put('donorId', '856'); req.put('campaignGoalId', 10); req.put('campaignMappingDisabled', false); frTestUtil.createTestPost(req); - + Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + String oppFrId = String.valueOf(req.get('id')); frTestUtil.assertNoErrors(); List gt = Database.query( @@ -234,47 +227,43 @@ public class frDonationTest { System.assertEquals(testAccount.Id, gt[0].get('DonorId')); System.assertEquals(testCampaign.Id, gt[0].get('CampaignId')); } - - // - // syncEntity_donationStatusDefaulting - // + + static testMethod void syncEntity_donationStatusDefaulting() { if (frUtil.hasNPCobjects()) { return; } Contact testContact = frDonorTest.getTestContact(); - + Campaign testCampaign = new Campaign(fr_ID__c = '10', Name = 'Test Campaign', ExpectedRevenue = 123, Description = 'Test', Status = 'Published'); insert testCampaign; - + createMapping('name', 'Name'); createMapping('donation_cretime', 'CloseDate'); - + Map request = getTestRequest(); request.put('status', 'Refunded'); frTestUtil.createTestPost(request); - + Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity newOpportunity = [SELECT Id, StageName, fr_ID__c, fr_Donor__c FROM Opportunity WHERE fr_Id__c = :oppFrId]; System.assertEquals('Closed Lost', newOpportunity.StageName, 'A status of refunded should have resulted in a Closed Lost stage'); } - - // - // syncEntity_newOpportunityRoleMappings - // + + static testMethod void syncEntity_newOpportunityRoleMappings() { if (frUtil.hasNPCobjects()) { return; } createMapping('name', 'Name'); createMapping('donation_cretime', 'CloseDate'); - + Contact donor = frDonorTest.getTestContact(false); donor.fr_Id__c = '1'; donor.Email = 'donor@example.com'; @@ -285,20 +274,20 @@ public class frDonationTest { teamCaptain.fr_Id__c = '3'; teamCaptain.Email = 'teamCaptain@example.com'; insert new List{donor, fundraiser, teamCaptain}; - - Map request = getTestRequest(); + + Map request = getTestRequest(); request.put('opportunityContactMappingDisabled', 'false'); request.put('donorId', donor.fr_Id__c); request.put('fundraiserId', fundraiser.fr_ID__c); request.put('teamCaptainId', teamCaptain.fr_ID__c); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity newOpportunity = [SELECT Id, fr_ID__c, fr_Donor__c, (SELECT Id, ContactId, Role FROM OpportunityContactRoles) FROM Opportunity WHERE fr_Id__c = :oppFrId]; List roles = newOpportunity.OpportunityContactRoles; @@ -315,17 +304,15 @@ public class frDonationTest { } } } - - // - // syncEntity_newOpportunityRoleMappings_missingContacts - // + + static testMethod void syncEntity_newOpportunityRoleMappings_missingContacts() { if (frUtil.hasNPCobjects()) { return; } createMapping('name', 'Name'); createMapping('donation_cretime', 'CloseDate'); - + Contact donor = frDonorTest.getTestContact(false); donor.fr_Id__c = '1'; donor.Email = 'donor@example.com'; @@ -336,55 +323,52 @@ public class frDonationTest { teamCaptain.fr_Id__c = '3'; teamCaptain.Email = 'teamCaptain@example.com'; insert new List{donor, fundraiser, teamCaptain}; - - Map request = getTestRequest(); + + Map request = getTestRequest(); request.put('opportunityContactMappingDisabled', 'false'); request.put('donorId', donor.fr_Id__c); request.put('fundraiserId', fundraiser.fr_ID__c+'1'); request.put('teamCaptainId', teamCaptain.fr_ID__c+'1'); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity newOpportunity = [SELECT Id, fr_ID__c, fr_Donor__c, (SELECT Id, ContactId, Role FROM OpportunityContactRoles) FROM Opportunity WHERE fr_Id__c = :oppFrId]; List roles = newOpportunity.OpportunityContactRoles; System.assertEquals(1, roles.size(), 'Since the request had ids that did not exist in SF, no roles should have been created except for the donor'); System.assertEquals(2, [SELECT COUNT() FROM Error__c], 'There should be 3 error logs for missing supporters that prevented opp roles from being created'); } - - // - // syncEntity_LinkToSubscription - // + static testMethod void syncEntity_LinkToSubscription() { if (frUtil.hasNPCobjects()) { return; } createMapping('name', 'Name'); createMapping('donation_cretime', 'CloseDate'); - + Contact testSupporter = frDonorTest.getTestContact(); - + Subscription__c subscription = new Subscription__c(Name = 'Test Sub', fr_ID__c = '1234', Supporter__c = testSupporter.Id); insert subscription; - + Map request = getTestRequest(); request.put('subscriptionId', subscription.fr_ID__c); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity newOpportunity = [SELECT Id, fr_ID__c, Subscription__c FROM Opportunity WHERE fr_Id__c = :oppFrId]; System.assertEquals(subscription.Id, newOpportunity.Subscription__c, 'The new donation should be pointing at the subscription corresponding to the id provided in the request'); } - + static testMethod void syncEntity_LinkToSubscription_NPC() { if (!frUtil.hasNPCobjects()) { return; @@ -393,65 +377,62 @@ public class frDonationTest { createMapping('name', 'Name'); createMapping('amount', 'OriginalAmount'); createMapping('donation_cretime', 'TransactionDueDate'); - + insert new frMapping__c(Type__c = 'Gift Transaction', Name = 'donation_cretime', fr_Name__c = 'donation_cretime', sf_Name__c = 'CheckDate'); - + Contact testSupporter = frDonorTest.getTestContact(); Account testAccount = frDonortest.getTestAccount(true); - + Subscription__c subscription = new Subscription__c(Name = 'Test Sub', fr_ID__c = '1234', Supporter__c = testSupporter.Id); insert subscription; - + SObject matchingNPCrecord = Schema.getGlobalDescribe().get('giftcommitment').newSObject(); matchingNPCrecord.put('fr_ID__c', '1234'); matchingNPCrecord.put('Name', 'Test Sub'); matchingNPCrecord.put('DonorId', testAccount.Id); insert matchingNPCrecord; - + Map request = getTestRequest(); request.put('subscriptionId', subscription.fr_ID__c); request.put('opportunityContactMappingDisabled', 'false'); request.put('donorId', '856'); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + String oppFrId = String.valueOf(request.get('id')); frTestUtil.assertNoErrors(); List gt = Database.query('SELECT Id, Name, DonorId, GiftCommitmentId FROM GiftTransaction WHERE fr_ID__c = :oppFrId'); List gc = Database.query('SELECT Id FROM GiftCommitment WHERE fr_ID__c = \'1234\''); System.assertEquals(gc[0].Id, gt[0].get('GiftCommitmentId')); } - - // - // syncEntity_LinkToPledge - // + static testMethod void syncEntity_LinkToPledge() { if (frUtil.hasNPCobjects()) { return; } createMapping('name', 'Name'); createMapping('donation_cretime', 'CloseDate'); - + Contact testSupporter = frDonorTest.getTestContact(); - + Pledge__c pledge = getTestPledge(testSupporter); insert pledge; - + frTestUtil.createTestPost(getTestRequest()); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity newOpportunity = [SELECT Id, fr_ID__c, Funraise_Pledge__c FROM Opportunity WHERE fr_Id__c = :oppFrId]; System.assertEquals(pledge.Id, newOpportunity.Funraise_Pledge__c, 'The new donation should be pointing at the pledge that the supporter had active'); } - + static testMethod void syncEntity_LinkToPledge_NPC() { if (!frUtil.hasNPCobjects()) { return; @@ -460,29 +441,26 @@ public class frDonationTest { insert frSetupController.getGiftTransactionDefaults(); insert new frMapping__c(Type__c = 'Gift Transaction', Name = 'donation_cretime', fr_Name__c = 'donation_cretime', sf_Name__c = 'CheckDate'); - + Contact testSupporter = frDonorTest.getTestContact(); Account testAccount = frDonortest.getTestAccount(true); - + Pledge__c pledge = getTestPledgeAcc(testAccount); insert pledge; - + Map request = getTestRequest(); request.put('donorId', '856'); request.put('pledge', true); frTestUtil.createTestPost(request); - + Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + List gt = Database.query('SELECT Id, OriginalAmount, fr_ID__c, Funraise_GT_Pledge__c FROM GiftTransaction'); System.assertNotEquals(null, gt[0].get('Funraise_GT_Pledge__c')); } - - // - // syncEntity_PledgeCreatesPledge - // + static testMethod void syncEntity_PledgeCreatesPledge() { if (frUtil.hasNPCobjects()) { return; @@ -490,17 +468,17 @@ public class frDonationTest { createMapping('name', 'Name'); createMapping('amount', 'amount'); createMapping('donation_cretime', 'CloseDate'); - + Contact testSupporter = frDonorTest.getTestContact(); - + Map request = getTestRequest(); request.put('pledge', true); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity opp = [SELECT Id, Amount, fr_ID__c, Funraise_Pledge__c FROM Opportunity WHERE fr_Id__c = :oppFrId]; @@ -512,7 +490,7 @@ public class frDonationTest { System.assertEquals(pledge.Pledge_Amount__c, opp.Amount, 'The pledged amount should be the same as the opportunity amount'); System.assertEquals(pledge.Received_Amount__c, opp.Amount, 'The receive amount should be the same as the opportunity amount since the opp is Closed Won'); } - + static testMethod void syncEntity_PledgeCreatesPledge_NPC() { if (!frUtil.hasNPCobjects()) { return; @@ -524,17 +502,17 @@ public class frDonationTest { insert frSetupController.getGiftTransactionDefaults(); insert new frMapping__c(Type__c = 'Gift Transaction', Name = 'donation_cretime', fr_Name__c = 'donation_cretime', sf_Name__c = 'CheckDate'); - + Map request = getTestRequest(); request.put('pledge', true); request.put('donorId', '856'); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); List gt = Database.query('SELECT Id, OriginalAmount, fr_ID__c, Funraise_GT_Pledge__c FROM GiftTransaction'); System.assertNotEquals(null, gt[0].get('Funraise_GT_Pledge__c')); @@ -545,7 +523,7 @@ public class frDonationTest { ]; System.assertEquals(gt[0].get('OriginalAmount'), pledge.Pledge_Amount__c); } - + // // syncEntity_PledgeUpdatesPledge // @@ -557,34 +535,34 @@ public class frDonationTest { createMapping('amount', 'amount'); createMapping('donation_cretime', 'CloseDate'); Contact testSupporter = frDonorTest.getTestContact(); - + Opportunity existingOpp = getTestOpp(); existingOpp.fr_Id__c = '2048'; insert existingOpp; - + Pledge__c pledge = getTestPledge(testSupporter); pledge.Pledge_Donation__c = existingOpp.Id; insert pledge; - + existingOpp.Funraise_Pledge__c = pledge.Id; update existingOpp; - + //requery to get the workflow rule update on pledge_donation_uq__c pledge = [SELECT Id, Pledge_Donation__c, Pledge_Donation_uq__c FROM Pledge__c WHERE Id = :pledge.Id]; System.assertEquals(pledge.Pledge_Donation__c +'', pledge.Pledge_Donation_uq__c+'', 'The external id unique field should have been updated with the value from the lookup field'); - - + + Map request = getTestRequest(); request.put('pledge', true); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity opp = [SELECT Id, Amount, fr_ID__c, Funraise_Pledge__c FROM Opportunity WHERE fr_Id__c = :oppFrId]; System.assertNotEquals(null, opp.Funraise_Pledge__c, 'The new donation should have created a new pledge'); @@ -595,48 +573,48 @@ public class frDonationTest { System.assertEquals(pledge.Pledge_Amount__c, opp.Amount, 'The pledged amount should be the same as the opportunity amount'); System.assertEquals(pledge.Received_Amount__c, opp.Amount, 'The receive amount should be the same as the opportunity amount since the opp is Closed Won'); } - + static testMethod void syncEntity_PledgeUpdatesPledge_NPC() { if (!frUtil.hasNPCobjects()) { return; } Account testAccount = frDonorTest.getTestAccount(true); - + createMapping('name', 'Name'); createMapping('amount', 'OriginalAmount'); insert frSetupController.getGiftTransactionDefaults(); insert new frMapping__c(Type__c = 'Gift Transaction', Name = 'donation_cretime', fr_Name__c = 'donation_cretime', sf_Name__c = 'CheckDate'); - + Opportunity existingOpp = getTestOpp(); existingOpp.fr_Id__c = '2048'; insert existingOpp; - + Contact testSupporter = frDonorTest.getTestContact(); Pledge__c pledge = getTestPledge(testSupporter); pledge.Pledge_Donation__c = existingOpp.Id; insert pledge; - + SObject existingGT = getTestGT(); existingGT.put('fr_Id__c', '2048'); existingGT.put('Funraise_GT_Pledge__c', pledge.Id); insert existingGT; - + existingOpp.Funraise_Pledge__c = pledge.Id; update existingOpp; existingGT.put('Funraise_GT_Pledge__c', pledge.Id); update existingGT; - + Map request = getTestRequest(); request.put('pledge', true); request.put('donorId', '856'); frTestUtil.createTestPost(request); - + Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); frTestUtil.assertNoErrors(); - + List gt = Database.query( 'SELECT Id, OriginalAmount, fr_ID__c, Funraise_GT_Pledge__c FROM GiftTransaction' ); @@ -648,7 +626,7 @@ public class frDonationTest { ]; System.assertEquals(gt[0].get('OriginalAmount'), pledge.Pledge_Amount__c); } - + // // syncEntity_existing_alreadyLinkedToPledge // @@ -659,7 +637,7 @@ public class frDonationTest { createMapping('name', 'Name'); createMapping('amount', 'amount'); createMapping('donation_cretime', 'CloseDate'); - + Contact testSupporter = frDonorTest.getTestContact(); Pledge__c pledge = getTestPledge(testSupporter); pledge.End_Date__c = Date.today().addDays(-1); //so it's no longer active @@ -668,22 +646,22 @@ public class frDonationTest { existingOpp.fr_Id__c = '2048'; existingOpp.Funraise_Pledge__c = pledge.Id; insert existingOpp; - + Map request = getTestRequest(); request.put('id', existingOpp.fr_Id__c); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity opp = [SELECT Id, Funraise_Pledge__c FROM Opportunity WHERE fr_Id__c = :oppFrId]; System.assertEquals(pledge.Id, opp.Funraise_Pledge__c, 'The pledge value should remain unchanged'); } - + static testMethod void syncEntity_existing_alreadyLinkedToPledge_NPC() { if (!frUtil.hasNPCobjects()) { return; @@ -692,34 +670,34 @@ public class frDonationTest { createMapping('amount', 'OriginalAmount'); createMapping('donation_cretime', 'CheckDate'); insert frSetupController.getGiftTransactionDefaults(); - + Contact testSupporter = frDonorTest.getTestContact(); Account testAccount = frDonortest.getTestAccount(true); Pledge__c pledge = getTestPledgeAcc(testAccount); pledge.End_Date__c = Date.today().addDays(-1); insert pledge; - + SObject existingGT = getTestGT(); existingGT.put('fr_Id__c', '2048'); existingGT.put('Funraise_GT_Pledge__c', pledge.Id); insert existingGT; - + Map request = getTestRequest(); request.put('id', existingGT.get('fr_Id__c')); request.put('donorId', '856'); frTestUtil.createTestPost(request); - + Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + String requestId = (String) request.get('id'); List gc = Database.query( 'SELECT Id, Funraise_GT_Pledge__c FROM GiftTransaction WHERE fr_ID__c = :requestId' ); System.assertEquals(pledge.Id, gc[0].get('Funraise_GT_Pledge__c'), 'The pledge value should remain unchanged'); } - + // // syncEntity_oldDonation_matchesInactivePledge // @@ -730,7 +708,7 @@ public class frDonationTest { createMapping('name', 'Name'); createMapping('amount', 'amount'); createMapping('donation_cretime', 'CloseDate'); - + Contact testSupporter = frDonorTest.getTestContact(); //get a pledge that is inactive according to dates //but is unfulfilled in amount @@ -738,22 +716,22 @@ public class frDonationTest { pledge.Start_Date__c = Date.today().addDays(-5); pledge.End_Date__c = Date.today().addDays(-1); insert pledge; - + Map request = getTestRequest(); request.put('donation_cretime', DateTime.now().addDays(-3).getTime()); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity opp = [SELECT Id, Funraise_Pledge__c FROM Opportunity WHERE fr_Id__c = :oppFrId]; System.assertEquals(pledge.Id, opp.Funraise_Pledge__c, 'The donation should have matched to a previously active unfulfilled pledge'); } - + static testMethod void syncEntity_oldDonation_matchesInactivePledge_NPC() { if (!frUtil.hasNPCobjects()) { return; @@ -763,24 +741,24 @@ public class frDonationTest { //createMapping('donation_cretime', 'TransactionDueDate'); createMapping('donation_cretime', 'CheckDate'); insert frSetupController.getGiftTransactionDefaults(); - + Contact testSupporter = frDonorTest.getTestContact(); Account testAccount = frDonortest.getTestAccount(true); Pledge__c pledge = getTestPledgeAcc(testAccount); pledge.Start_Date__c = Date.today().addDays(-5); pledge.End_Date__c = Date.today().addDays(-1); insert pledge; - + Map req = getTestRequest(); req.put('donation_cretime', DateTime.now().addDays(-3).getTime()); req.put('donorId', testAccount.get('fr_Id__c')); frTestUtil.createTestPost(req); - + Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); frTestUtil.assertNoErrors(); - + String oppFrId = String.valueOf(req.get('id')); List gc = Database.query( 'SELECT Funraise_GT_Pledge__c ' + @@ -789,7 +767,7 @@ public class frDonationTest { ); System.assertEquals(pledge.Id, gc[0].get('Funraise_GT_Pledge__c'), 'The donation should have matched to a previously active unfulfilled pledge'); } - + // // syncEntity_oldDonation_doesNotMatchInactivePledge // @@ -800,7 +778,7 @@ public class frDonationTest { createMapping('name', 'Name'); createMapping('amount', 'amount'); createMapping('donation_cretime', 'CloseDate'); - + Contact testSupporter = frDonorTest.getTestContact(); //get a pledge that is inactive according to dates //but is unfulfilled in amount @@ -808,22 +786,22 @@ public class frDonationTest { pledge.Start_Date__c = Date.today().addDays(-5); pledge.End_Date__c = Date.today().addDays(-1); insert pledge; - + Map request = getTestRequest(); request.put('donation_cretime', DateTime.now().getTime()); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); Opportunity opp = [SELECT Id, Funraise_Pledge__c FROM Opportunity WHERE fr_Id__c = :oppFrId]; System.assertEquals(null, opp.Funraise_Pledge__c, 'The donation should not have matched to a previously active unfulfilled pledge'); } - + static testMethod void syncEntity_oldDonation_doesNotMatchInactivePledge_NPC() { if (!frUtil.hasNPCobjects()) { return; @@ -831,27 +809,27 @@ public class frDonationTest { createMapping('name', 'Name'); createMapping('amount', 'OriginalAmount'); insert frSetupController.getGiftTransactionDefaults(); - + Contact testSupporter = frDonorTest.getTestContact(); Account testAccount = frDonortest.getTestAccount(true); Pledge__c pledge = getTestPledgeAcc(testAccount); pledge.Start_Date__c = Date.today().addDays(-5); pledge.End_Date__c = Date.today().addDays(-1); insert pledge; - + Map req = getTestRequest(); req.put('donation_cretime', DateTime.now().getTime()); req.put('donorId', '856'); frTestUtil.createTestPost(req); - + Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + // The original code didn't do an assertion for the else block // If you'd like to assert it's null, you can do so } - + // // syncEntity_oppContactRoles_sameSupporter // @@ -863,42 +841,42 @@ public class frDonationTest { createMapping('amount', 'amount'); createMapping('donation_cretime', 'CloseDate'); Contact testSupporter = frDonorTest.getTestContact(); - + Map request = getTestRequest(); request.put('opportunityContactMappingDisabled', false); request.put('donorId', testSupporter.fr_Id__c); request.put('fundraiserId', testSupporter.fr_Id__c); request.put('teamCaptainId', testSupporter.fr_Id__c); request.put('softCreditSupporterId', testSupporter.fr_Id__c); - + frTestUtil.createTestPost(request); Test.startTest(); frWSDonationController.syncEntity(); Test.stopTest(); - + frTestUtil.assertNoErrors(); - + String oppFrId = String.valueOf(getTestRequest().get('id')); Set funraiseRoles = new Set{ frDonation.OPP_ROLE_DONOR, - frDonation.OPP_ROLE_FUNDRAISER, - frDonation.OPP_ROLE_SOFT_CREDIT, - frDonation.OPP_ROLE_TEAM_CAPTAIN - }; - Opportunity opp = [SELECT Id, Funraise_Pledge__c, - (SELECT Id, Role FROM OpportunityContactRoles WHERE ContactId = :testSupporter.Id AND role IN :funraiseRoles) - FROM Opportunity WHERE fr_Id__c = :oppFrId]; + frDonation.OPP_ROLE_FUNDRAISER, + frDonation.OPP_ROLE_SOFT_CREDIT, + frDonation.OPP_ROLE_TEAM_CAPTAIN + }; + Opportunity opp = [SELECT Id, Funraise_Pledge__c, + (SELECT Id, Role FROM OpportunityContactRoles WHERE ContactId = :testSupporter.Id AND role IN :funraiseRoles) + FROM Opportunity WHERE fr_Id__c = :oppFrId]; System.assertEquals(1, opp.OpportunityContactRoles.size(), 'There should be a single opp contact role for the supporter'); System.assertEquals(frDonation.OPP_ROLE_DONOR, opp.OpportunityContactRoles.get(0).role, - 'The single opp contact role should be the donor role'); + 'The single opp contact role should be the donor role'); } - + static testMethod void syncEntity_setOpportunityStage_NPC() { if (!frUtil.hasNPCobjects()) { return; } - + createMapping('name', 'Name'); insert frSetupController.getGiftTransactionDefaults(); Contact testContact = frDonorTest.getTestContact(); @@ -908,13 +886,13 @@ public class frDonationTest { insert testCampaign; Subscription__c subscription = new Subscription__c(Name = 'Test Sub', fr_ID__c = '1234', Supporter__c = testContact.Id); insert subscription; - + Map request = getTestRequest(); request.put('subscriptionId', subscription.fr_ID__c); request.put('opportunityContactMappingDisabled', 'false'); request.put('donorId', '856'); frTestUtil.createTestPost(request); - + Test.startTest(); Sync_Attempt__c syncRecord = new Sync_Attempt__c( Request_Body__c = RestContext.request.requestBody.toString(), @@ -927,10 +905,56 @@ public class frDonationTest { frd.setOpportunityStage(new Opportunity(), 'Pending'); frd.setOpportunityStage(new Opportunity(), 'Refunded'); Test.stopTest(); - + + frTestUtil.assertNoErrors(); + } + + static testMethod void syncEntity_constantMapping_noOverwrite() { + if (frUtil.hasNPCobjects()) { + return; // skip in NPC org + } + String constantValue = 'test description'; + String updatedValue = 'test updated description'; + createMapping('name', 'Name'); + createMapping('donation_cretime', 'CloseDate'); + insert new frMapping__c( + Name = 'Description No Overwrite', + Is_Constant__c = true, + Constant_Value__c = constantValue, + sf_Name__c = 'Description', + Type__c = frDonation.TYPE, + Conflict_Resolution__c = frModel.MAPPING_NO_OVERWRITE + ); + + Map request = getTestRequest(); + frTestUtil.createTestPost(request); + Test.startTest(); + frWSDonationController.syncEntity(); + + Opportunity opportunity = [ + SELECT Id, Description + FROM Opportunity + WHERE fr_Id__c = :String.valueOf(request.get('id')) + ]; + //since this was a new record, there was no data that would have been overwritten so the constant value should be used + System.assertEquals(constantValue, opportunity.Description, 'Constant mapping should populate the initial value'); + + opportunity.Description = updatedValue; + update opportunity; + + frTestUtil.createTestPost(request); + frWSDonationController.syncEntity(); + Test.stopTest(); + + Opportunity updatedOpportunity = [ + SELECT Description + FROM Opportunity + WHERE Id = :opportunity.Id + ]; + System.assertEquals(updatedValue, updatedOpportunity.Description, 'Constant mapping with NO_OVERWRITE should not overwrite existing values'); frTestUtil.assertNoErrors(); } - + // // Utility methods // @@ -942,7 +966,7 @@ public class frDonationTest { Type__c = frDonation.TYPE ); } - + public static Map getTestRequest() { Map request = new Map(); request.put('id', 2048); @@ -975,7 +999,7 @@ public class frDonationTest { request.put('paymentMethodType', 'Cash'); return request; } - + public static Opportunity getTestOpp() { return new Opportunity( CloseDate = Date.today(), @@ -984,7 +1008,7 @@ public class frDonationTest { fr_Id__c = '19931107' ); } - + public static SObject getTestGT() { SObject o = Schema.getGlobalDescribe().get('gifttransaction').newSObject(); o.put('Name', 'Unit Test Opportunity'); @@ -995,14 +1019,14 @@ public class frDonationTest { o.put('PaymentMethod', 'Cash'); return o; } - + public static Pledge__c getTestPledge(Contact supporter) { return new Pledge__c( Supporter__c = supporter.Id, Pledge_Amount__c = 500 ); } - + public static Pledge__c getTestPledgeAcc(Account supporter) { return new Pledge__c( Supporter__c = (String) supporter.get('PersonContactId'), diff --git a/src/classes/frModel.cls b/src/classes/frModel.cls index f44fa09..3233c1d 100644 --- a/src/classes/frModel.cls +++ b/src/classes/frModel.cls @@ -138,20 +138,7 @@ public with sharing abstract class frModel { for(frMapping__c mapping : frFieldToMappings.get(fieldName)) { Schema.SObjectField field = fields.get(mapping.sf_Name__c); Object incomingValue = request.get(fieldName); - Boolean write = false; - if(String.isBlank(mapping.Conflict_Resolution__c) || mapping.Conflict_Resolution__c == MAPPING_OVERWRITE) { - write = true; - } else if (mapping.Conflict_Resolution__c == MAPPING_OVERWRITE_NON_NULL && (incomingValue != null || (incomingValue instanceOf String && String.isNotBlank((String)incomingValue)))) { - write = true; - } else if (mapping.Conflict_Resolution__c == MAPPING_NO_OVERWRITE) { - Object existingValue = queriedFieldsRecord != null ? queriedFieldsRecord.get(field) : null; - write = existingValue == null; - } else if (mapping.Conflict_Resolution__c == MAPPING_OVERWRITE_RECENT && isMoreRecent) { - write = true; - } else if (String.isNotBlank(mapping.Conflict_Resolution__c) && !VALID_CONFLICT_RESOLUTIONS.contains(mapping.Conflict_Resolution__c)) { - insert new Error__c(Error__c = 'Field mapping exception. Unknown conflict resolution provided. Object type: '+ sObjectName + - ' - Field: '+field.getDescribe().getName() + 'Conflict Resolution: '+ mapping.Conflict_Resolution__c); - } + Boolean write = shouldWriteMapping(mapping, incomingValue, queriedFieldsRecord, isMoreRecent, field, sObjectName); if(write) { write(record, field, mapping.sf_Name__c, incomingValue, funraiseId); } @@ -160,11 +147,31 @@ public with sharing abstract class frModel { } for(frMapping__c constantMapping : constantMappings) { Schema.SObjectField field = fields.get(constantMapping.sf_Name__c); - write(record, field, constantMapping.sf_Name__c, constantMapping.Constant_Value__c, funraiseId); - } + Boolean write = shouldWriteMapping(constantMapping, constantMapping.Constant_Value__c, queriedFieldsRecord, isMoreRecent, field, sObjectName); + if(write) { + write(record, field, constantMapping.sf_Name__c, constantMapping.Constant_Value__c, funraiseId); + } } record.put('fr_ID__c', funraiseId); } + private static Boolean shouldWriteMapping(frMapping__c mapping, Object incomingValue, SObject queriedFieldsRecord, Boolean isMoreRecent, Schema.SObjectField field, String sObjectName) { + Boolean write = false; + if(String.isBlank(mapping.Conflict_Resolution__c) || mapping.Conflict_Resolution__c == MAPPING_OVERWRITE) { + write = true; + } else if (mapping.Conflict_Resolution__c == MAPPING_OVERWRITE_NON_NULL && (incomingValue != null || (incomingValue instanceOf String && String.isNotBlank((String)incomingValue)))) { + write = true; + } else if (mapping.Conflict_Resolution__c == MAPPING_NO_OVERWRITE) { + Object existingValue = queriedFieldsRecord != null ? queriedFieldsRecord.get(field) : null; + write = existingValue == null; + } else if (mapping.Conflict_Resolution__c == MAPPING_OVERWRITE_RECENT && isMoreRecent) { + write = true; + } else if (String.isNotBlank(mapping.Conflict_Resolution__c) && !VALID_CONFLICT_RESOLUTIONS.contains(mapping.Conflict_Resolution__c)) { + insert new Error__c(Error__c = 'Field mapping exception. Unknown conflict resolution provided. Object type: '+ sObjectName + + ' - Field: '+field.getDescribe().getName() + 'Conflict Resolution: '+ mapping.Conflict_Resolution__c); + } + return write; + } + public static void write(SObject record, Schema.SObjectField field, String fieldName, Object value, String funraiseId) { try { if (fieldName.toLowerCase() == 'id') { From 11df1f933f7575a92b9c5842125668cd50328c9a Mon Sep 17 00:00:00 2001 From: Alejandro Molina Date: Mon, 15 Dec 2025 13:47:08 -0700 Subject: [PATCH 2/4] [FUN-16422] Fix logic for determining when to automatically populate the stage Previously if the stage was populated, an updated status wouldn't cause our logic to re-evaluate what stage it should be. Now a status update from Funraise will update the opportunity correctly (if stage is unmapped) --- src/classes/frDonation.cls | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/classes/frDonation.cls b/src/classes/frDonation.cls index 8a3bfa3..5b6f7ad 100644 --- a/src/classes/frDonation.cls +++ b/src/classes/frDonation.cls @@ -170,9 +170,19 @@ public class frDonation extends frModel implements frSyncable { return result; } + @testVisible + private Boolean shouldSetOpportunityStage() { + for(frMapping__c mapping : getMappings()) { + if(mapping.sf_Name__c == 'StageName') { + return false; + } + } + return true; + } + @testVisible private void setOpportunityStage(Opportunity o, String status) { - if(String.isBlank(o.StageName) && (String.isBlank([SELECT StageName from Opportunity where fr_Id__c = :o.fr_Id__c]?.StageName))) { + if(shouldSetOpportunityStage()) { List stages = [SELECT Id, MasterLabel, IsWon, IsClosed FROM OpportunityStage WHERE IsActive = true From 9937c7e06ab650314a0a1481d0bb5a5a72be37a8 Mon Sep 17 00:00:00 2001 From: Alejandro Molina Date: Mon, 15 Dec 2025 13:51:58 -0700 Subject: [PATCH 3/4] [FUN-16328] Set ordering for Contact Matching queries Always use the most recently created Contact for matching to It looks like the server reformatted all of the whitespace in this file the relevant changes are just in the SOQL queries --- src/classes/frDonor.cls | 508 ++++++++++++++++++------------------ src/classes/frDonorTest.cls | 39 +++ 2 files changed, 293 insertions(+), 254 deletions(-) diff --git a/src/classes/frDonor.cls b/src/classes/frDonor.cls index 5e269fe..5b10717 100644 --- a/src/classes/frDonor.cls +++ b/src/classes/frDonor.cls @@ -1,255 +1,255 @@ -/* -* -* Copyright (c) 2020, Funraise Inc -* All rights reserved. -* -* Redistribution and use in source and binary forms, with or without -* modification, are permitted provided that the following conditions are met: -* 1. Redistributions of source code must retain the above copyright -* notice, this list of conditions and the following disclaimer. -* 2. Redistributions in binary form must reproduce the above copyright -* notice, this list of conditions and the following disclaimer in the -* documentation and/or other materials provided with the distribution. -* 3. All advertising materials mentioning features or use of this software -* must display the following acknowledgement: -* This product includes software developed by the . -* 4. Neither the name of the nor the -* names of its contributors may be used to endorse or promote products -* derived from this software without specific prior written permission. -* -* THIS SOFTWARE IS PROVIDED BY FUNRAISE INC ''AS IS'' AND ANY -* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -* DISCLAIMED. IN NO EVENT SHALL FUNRAISE INC BE LIABLE FOR ANY -* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -* -* -* -* PURPOSE: -* -* -* -* CREATED: 2016 Funraise Inc - https://funraise.io -* AUTHOR: Jason M. Swenski -*/ - -public class frDonor extends frModel implements frSyncable{ - public static final String TYPE = 'Donor'; - public static final String SOBJ_NAME = frUtil.hasNPCobjects() ? 'Account' : 'Contact'; - - public static List mappings { - get { - if(mappings == null) { - mappings = [SELECT fr_Name__c, sf_Name__c, Is_Constant__c, Constant_Value__c, Conflict_Resolution__c, Type__c FROM frMapping__c WHERE Type__c = :TYPE ORDER BY CreatedDate]; - } - return mappings; - } - set; - } - - public override List getMappings() { - return mappings; - } - - private SObject supporter; - - public frDonor(Sync_Attempt__c syncRecord){ - super(syncRecord); - } - - public Boolean sync() { - Boolean result = false; - Map request = getRequestBody(); - String frId = getFunraiseId(); - - supporter = null; // Generic SObject for both Account and Contact - Boolean isNPC = frUtil.hasNPCobjects(); - - // Query based on fr_ID__c - List records = Database.query( - 'SELECT Id, fr_ID__c, LastName FROM ' + SOBJ_NAME + ' WHERE fr_ID__c = :frId' - ); - - if (records != null && !records.isEmpty()) { - supporter = records.get(0); - } - - String firstName = String.valueOf(request.get('firstName')); - String lastName = String.valueOf(request.get('lastName')); - String email = String.valueOf(request.get('email')); - - // Dynamic email field based on whether Person Accounts are enabled - String emailField = isNPC ? 'PersonEmail' : 'Email'; - - // Match based on email and names - if (supporter == null && String.isNotBlank(email)) { - String query = 'SELECT Id, fr_ID__c, LastName FROM ' + SOBJ_NAME + ' WHERE ' + emailField + ' = :email'; - - if (String.isNotBlank(firstName) && String.isNotBlank(lastName)) { - query += ' AND FirstName = :firstName AND LastName = :lastName'; - - query += ' LIMIT 1'; - - records = Database.query(query); - if(isNPC) { - supporter = applyAccountMatch(records, frId); - } - else { - supporter = applyMatch(records, frId); - } - } - } - //match on just email - if (supporter == null && String.isNotBlank(email)) { - String query = 'SELECT Id, fr_ID__c, LastName FROM ' + SOBJ_NAME + ' WHERE ' + emailField + ' = :email'; - - query += ' LIMIT 1'; - - records = Database.query(query); - if(isNPC) { - supporter = applyAccountMatch(records, frId); - } - else { - supporter = applyMatch(records, frId); - } - } - - // If still not found, try matching based on address - if (supporter == null) { - String address1 = String.valueOf(request.get('address1')); - String city = String.valueOf(request.get('city')); - String state = String.valueOf(request.get('state')); - String postalCode = String.valueOf(request.get('postalCode')); - - Boolean namePresent = String.isNotBlank(firstName) && String.isNotBlank(lastName); - Boolean cityAndState = String.isNotBlank(city) && String.isNotBlank(state); - - String mailingStreetField = isNPC ? 'PersonMailingStreet' : 'MailingStreet'; - String mailingCityField = isNPC ? 'PersonMailingCity' : 'MailingCity'; - String mailingStateField = isNPC ? 'PersonMailingState' : 'MailingState'; - String mailingPostalCodeField = isNPC ? 'PersonMailingPostalCode' : 'MailingPostalCode'; - - if (namePresent && String.isNotBlank(address1) && (cityAndState || String.isNotBlank(postalCode))) { - String byAddress = 'SELECT Id, fr_ID__c, LastName FROM ' + SOBJ_NAME + - ' WHERE ' + mailingStreetField + ' = :address1 AND FirstName = :firstName AND LastName = :lastName'; - - if (cityAndState) { - byAddress += ' AND ' + mailingCityField + ' = :city AND ' + mailingStateField + ' = :state'; - } - if (String.isNotBlank(postalCode)) { - byAddress += ' AND ' + mailingPostalCodeField + ' = :postalCode'; - } - byAddress += ' LIMIT 1'; - - records = Database.query(byAddress); - supporter = applyMatch(records, frId); - } - } - - // If no match, create a new record - if (supporter == null) { - if(isNPC) { - supporter = new Account(fr_Id__c = frId); - } - else { - supporter = new Contact(fr_Id__c = frId); - } - } - - // Apply mappings - applyMappings(supporter, request); - - if (String.isBlank((String)supporter.get('LastName'))) { - supporter.put('LastName', String.valueOf(request.get('institutionName'))); - } - // Perform upsert operation - try { - if (supporter.Id != null) { - Database.update(supporter, true); - } else { - if(frUtil.hasNPCobjects()) Database.upsert(supporter, true); - else Database.upsert(supporter, Contact.Fields.fr_ID__c, true); - } - result = true; - } catch (Exception ex) { - if (createLogRecord) { - frUtil.logException(getFrType(), frId, ex); - } - } - - return result; - } - - private Contact applyMatch(List matchResults, String frId) { - if(matchResults != null && matchResults.size() > 0) { - Contact donor = matchResults.get(0); - donor.fr_ID__c = frId; - return donor; - } - return null; - } - - private Account applyAccountMatch(List matchResults, String frId) { - if(matchResults != null && matchResults.size() > 0) { - Account donor = matchResults.get(0); - donor.fr_ID__c = frId; - return donor; - } - return null; - } - - protected override String getSalesforceId() { - return supporter?.Id; - } - - protected override Set getFields() { - Map fields = frSchemaUtil.getFields(Contact.sObjectType.getDescribe().getName()); - Set usedFields = new Set(); - for(frMapping__c mapping : getMappings()) { - if(fields.containsKey(mapping.sf_Name__c)) { - usedFields.add(fields.get(mapping.sf_Name__c)); - } - } - usedFields.add(Contact.fr_Id__c); - usedFields.add(Contact.Email); - usedFields.add(Contact.FirstName); - usedFields.add(Contact.LastName); - usedFields.add(Contact.MailingStreet); - usedFields.add(Contact.MailingCity); - usedFields.add(Contact.MailingState); - usedFields.add(Contact.MailingPostalCode); - usedFields.add(Contact.MailingCountry); - if(frUtil.hasNPCobjects()) { - usedFields = new Set(); - fields = frSchemaUtil.getFields(Account.sObjectType.getDescribe().getName()); - for(frMapping__c mapping : getMappings()) { - if(fields.containsKey(mapping.sf_Name__c)) { - usedFields.add(fields.get(mapping.sf_Name__c)); - } - } - } - return usedFields; - } - - protected override Set getObjects() { - if(!frUtil.hasNPCobjects()) { - return new Set { - Contact.SObjectType - }; - } - else { - return new Set { - Account.SObjectType - }; - } - } - - protected override frUtil.Entity getFrType() { - return frUtil.Entity.SUPPORTER; - } +/* +* +* Copyright (c) 2020, Funraise Inc +* All rights reserved. +* +* Redistribution and use in source and binary forms, with or without +* modification, are permitted provided that the following conditions are met: +* 1. Redistributions of source code must retain the above copyright +* notice, this list of conditions and the following disclaimer. +* 2. Redistributions in binary form must reproduce the above copyright +* notice, this list of conditions and the following disclaimer in the +* documentation and/or other materials provided with the distribution. +* 3. All advertising materials mentioning features or use of this software +* must display the following acknowledgement: +* This product includes software developed by the . +* 4. Neither the name of the nor the +* names of its contributors may be used to endorse or promote products +* derived from this software without specific prior written permission. +* +* THIS SOFTWARE IS PROVIDED BY FUNRAISE INC ''AS IS'' AND ANY +* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +* DISCLAIMED. IN NO EVENT SHALL FUNRAISE INC BE LIABLE FOR ANY +* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +* +* +* +* PURPOSE: +* +* +* +* CREATED: 2016 Funraise Inc - https://funraise.io +* AUTHOR: Jason M. Swenski +*/ + +public class frDonor extends frModel implements frSyncable{ + public static final String TYPE = 'Donor'; + public static final String SOBJ_NAME = frUtil.hasNPCobjects() ? 'Account' : 'Contact'; + + public static List mappings { + get { + if(mappings == null) { + mappings = [SELECT fr_Name__c, sf_Name__c, Is_Constant__c, Constant_Value__c, Conflict_Resolution__c, Type__c FROM frMapping__c WHERE Type__c = :TYPE ORDER BY CreatedDate]; + } + return mappings; + } + set; + } + + public override List getMappings() { + return mappings; + } + + private SObject supporter; + + public frDonor(Sync_Attempt__c syncRecord){ + super(syncRecord); + } + + public Boolean sync() { + Boolean result = false; + Map request = getRequestBody(); + String frId = getFunraiseId(); + + supporter = null; // Generic SObject for both Account and Contact + Boolean isNPC = frUtil.hasNPCobjects(); + + // Query based on fr_ID__c + List records = Database.query( + 'SELECT Id, fr_ID__c, LastName FROM ' + SOBJ_NAME + ' WHERE fr_ID__c = :frId' + ); + + if (records != null && !records.isEmpty()) { + supporter = records.get(0); + } + + String firstName = String.valueOf(request.get('firstName')); + String lastName = String.valueOf(request.get('lastName')); + String email = String.valueOf(request.get('email')); + + // Dynamic email field based on whether Person Accounts are enabled + String emailField = isNPC ? 'PersonEmail' : 'Email'; + + // Match based on email and names + if (supporter == null && String.isNotBlank(email)) { + String query = 'SELECT Id, fr_ID__c, LastName FROM ' + SOBJ_NAME + ' WHERE ' + emailField + ' = :email'; + + if (String.isNotBlank(firstName) && String.isNotBlank(lastName)) { + query += ' AND FirstName = :firstName AND LastName = :lastName'; + + query += ' ORDER BY CreatedDate DESC LIMIT 1'; + + records = Database.query(query); + if(isNPC) { + supporter = applyAccountMatch(records, frId); + } + else { + supporter = applyMatch(records, frId); + } + } + } + //match on just email + if (supporter == null && String.isNotBlank(email)) { + String query = 'SELECT Id, fr_ID__c, LastName FROM ' + SOBJ_NAME + ' WHERE ' + emailField + ' = :email'; + + query += ' ORDER BY CreatedDate DESC LIMIT 1'; + + records = Database.query(query); + if(isNPC) { + supporter = applyAccountMatch(records, frId); + } + else { + supporter = applyMatch(records, frId); + } + } + + // If still not found, try matching based on address + if (supporter == null) { + String address1 = String.valueOf(request.get('address1')); + String city = String.valueOf(request.get('city')); + String state = String.valueOf(request.get('state')); + String postalCode = String.valueOf(request.get('postalCode')); + + Boolean namePresent = String.isNotBlank(firstName) && String.isNotBlank(lastName); + Boolean cityAndState = String.isNotBlank(city) && String.isNotBlank(state); + + String mailingStreetField = isNPC ? 'PersonMailingStreet' : 'MailingStreet'; + String mailingCityField = isNPC ? 'PersonMailingCity' : 'MailingCity'; + String mailingStateField = isNPC ? 'PersonMailingState' : 'MailingState'; + String mailingPostalCodeField = isNPC ? 'PersonMailingPostalCode' : 'MailingPostalCode'; + + if (namePresent && String.isNotBlank(address1) && (cityAndState || String.isNotBlank(postalCode))) { + String byAddress = 'SELECT Id, fr_ID__c, LastName FROM ' + SOBJ_NAME + + ' WHERE ' + mailingStreetField + ' = :address1 AND FirstName = :firstName AND LastName = :lastName'; + + if (cityAndState) { + byAddress += ' AND ' + mailingCityField + ' = :city AND ' + mailingStateField + ' = :state'; + } + if (String.isNotBlank(postalCode)) { + byAddress += ' AND ' + mailingPostalCodeField + ' = :postalCode'; + } + byAddress += ' ORDER BY CreatedDate DESC LIMIT 1'; + + records = Database.query(byAddress); + supporter = applyMatch(records, frId); + } + } + + // If no match, create a new record + if (supporter == null) { + if(isNPC) { + supporter = new Account(fr_Id__c = frId); + } + else { + supporter = new Contact(fr_Id__c = frId); + } + } + + // Apply mappings + applyMappings(supporter, request); + + if (String.isBlank((String)supporter.get('LastName'))) { + supporter.put('LastName', String.valueOf(request.get('institutionName'))); + } + // Perform upsert operation + try { + if (supporter.Id != null) { + Database.update(supporter, true); + } else { + if(frUtil.hasNPCobjects()) Database.upsert(supporter, true); + else Database.upsert(supporter, Contact.Fields.fr_ID__c, true); + } + result = true; + } catch (Exception ex) { + if (createLogRecord) { + frUtil.logException(getFrType(), frId, ex); + } + } + + return result; + } + + private Contact applyMatch(List matchResults, String frId) { + if(matchResults != null && matchResults.size() > 0) { + Contact donor = matchResults.get(0); + donor.fr_ID__c = frId; + return donor; + } + return null; + } + + private Account applyAccountMatch(List matchResults, String frId) { + if(matchResults != null && matchResults.size() > 0) { + Account donor = matchResults.get(0); + donor.fr_ID__c = frId; + return donor; + } + return null; + } + + protected override String getSalesforceId() { + return supporter?.Id; + } + + protected override Set getFields() { + Map fields = frSchemaUtil.getFields(Contact.sObjectType.getDescribe().getName()); + Set usedFields = new Set(); + for(frMapping__c mapping : getMappings()) { + if(fields.containsKey(mapping.sf_Name__c)) { + usedFields.add(fields.get(mapping.sf_Name__c)); + } + } + usedFields.add(Contact.fr_Id__c); + usedFields.add(Contact.Email); + usedFields.add(Contact.FirstName); + usedFields.add(Contact.LastName); + usedFields.add(Contact.MailingStreet); + usedFields.add(Contact.MailingCity); + usedFields.add(Contact.MailingState); + usedFields.add(Contact.MailingPostalCode); + usedFields.add(Contact.MailingCountry); + if(frUtil.hasNPCobjects()) { + usedFields = new Set(); + fields = frSchemaUtil.getFields(Account.sObjectType.getDescribe().getName()); + for(frMapping__c mapping : getMappings()) { + if(fields.containsKey(mapping.sf_Name__c)) { + usedFields.add(fields.get(mapping.sf_Name__c)); + } + } + } + return usedFields; + } + + protected override Set getObjects() { + if(!frUtil.hasNPCobjects()) { + return new Set { + Contact.SObjectType + }; + } + else { + return new Set { + Account.SObjectType + }; + } + } + + protected override frUtil.Entity getFrType() { + return frUtil.Entity.SUPPORTER; + } } \ No newline at end of file diff --git a/src/classes/frDonorTest.cls b/src/classes/frDonorTest.cls index fbc1954..8a51419 100644 --- a/src/classes/frDonorTest.cls +++ b/src/classes/frDonorTest.cls @@ -325,6 +325,45 @@ public class frDonorTest { Integer countAfterSync = [SELECT COUNT() FROM Contact]; System.assertEquals(countBeforeSync, countAfterSync, 'No additional contacts should have been created'); } + + static testMethod void syncEntity_existing_match_email_prefers_most_recent() { + if (frUtil.hasNPCobjects()) { + return; + } + createMapping('firstName', 'FirstName'); + createMapping('lastName', 'LastName'); + createMapping('email', 'email'); + createMapping('address1', 'MailingStreet'); + createMapping('city', 'MailingCity'); + createMapping('state', 'MailingState'); + createMapping('postalCode', 'MailingPostalCode'); + createMapping('country', 'MailingCountry'); + + Contact older = new Contact(LastName = 'Test', FirstName = 'Existing', Email = 'alextest02221503@example.com'); + insert older; + Test.setCreatedDate(older.Id, DateTime.now().addDays(-1)); + + Contact newer = new Contact(LastName = 'Test', FirstName = 'Existing', Email = 'alextest02221503@example.com'); + try { + insert newer; + } catch (Exception ex) { + //the environment we're running in probably has duplicate rules on email address and rejected saving the second contact + //ordering email matches based on email won't be needed in this environment then + //consider the test successful + return; + } + Test.setCreatedDate(newer.Id, DateTime.now()); + + frTestUtil.createTestPost(getTestRequest()); + Test.startTest(); + frWSDonorController.syncEntity(); + Test.stopTest(); + frTestUtil.assertNoErrors(); + + String frId = String.valueOf(getTestRequest().get('id')); + Contact syncedContact = [SELECT Id, fr_ID__c FROM Contact WHERE fr_Id__c = :frId]; + System.assertEquals(newer.Id, syncedContact.Id, 'The most recently created contact should be used when multiple matches exist'); + } static testMethod void syncEntity_existing_match_email_NPC() { if (!frUtil.hasNPCobjects()) { From d109af176ba113bf8f0d858748b58e1b01b84e78 Mon Sep 17 00:00:00 2001 From: Alejandro Molina Date: Mon, 15 Dec 2025 13:53:00 -0700 Subject: [PATCH 4/4] Adding changes from the server also adding a mise.toml file for specifying the languages needed for using the Salesforce Ant Migration tool for pulling/pushing changes to the Salesforce server --- mise.toml | 3 +++ src/permissionsets/Funraise_Permission_Set.permissionset | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 mise.toml diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..14a4b15 --- /dev/null +++ b/mise.toml @@ -0,0 +1,3 @@ +[tools] +ant = "latest" +java = "latest" diff --git a/src/permissionsets/Funraise_Permission_Set.permissionset b/src/permissionsets/Funraise_Permission_Set.permissionset index c92af02..574b22b 100644 --- a/src/permissionsets/Funraise_Permission_Set.permissionset +++ b/src/permissionsets/Funraise_Permission_Set.permissionset @@ -627,7 +627,7 @@ true false Fundraising_Event_Registration__c - false + true true @@ -636,7 +636,7 @@ true false Fundraising_Event__c - false + true true