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/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 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/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()) { 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') { 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