Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

21367: [Spike] Nightly job to filter and sync contacts #241

Merged
merged 38 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b30c472
21367: Added dataSync services/controller to sync contacts data from …
VSydor Aug 28, 2024
448d9f8
21368: Using Xero's bulk method updateOrCreateContacts; added contact…
VSydor Sep 11, 2024
735602b
21368: Compilation temp fixes
VSydor Sep 12, 2024
3c3d7be
21368: Getting/syncing opps for xero/stripe
VSydor Sep 17, 2024
24ad235
21368: Code review comments, part 2
VSydor Sep 18, 2024
16cb691
21368: Getting contacts for donations before donations sync
VSydor Sep 23, 2024
72c7471
21368: Updated contact's accountNumber / name setup; Donations mappin…
VSydor Sep 25, 2024
5dc1066
deleting commented-out code
brmeyer Sep 26, 2024
edefa6d
correcting the getDonorContacts SFDC query + disabling Xero's SUMMARI…
brmeyer Sep 26, 2024
f396846
21368: Collecting contact ids from 400 Xero response and then retryin…
VSydor Sep 26, 2024
d412083
21368: retrying contact update in case of error response
VSydor Sep 27, 2024
d269a88
21368: processing update response to get failed to update contacts' ids
VSydor Sep 27, 2024
4100ad5
21368: getting contact by AN if AN already exists, then update by id
VSydor Sep 27, 2024
2c9181a
21368: code review comments for opp mappings
VSydor Sep 27, 2024
1df0a74
21368: code review comments/refactoring
VSydor Sep 29, 2024
2fb6d27
21368: getting accounts that don't have contacts
VSydor Sep 30, 2024
77bfe85
21368: added endpoint to trigger transactions sync
VSydor Sep 30, 2024
cb73497
21368: took sfdcClient updates from master
VSydor Sep 30, 2024
a116177
21368: testing updates
VSydor Sep 30, 2024
17d8b77
21368: getting existing invoices for accounting transactions
VSydor Sep 30, 2024
6f83ec2
21368: including archived contacts into contact search
VSydor Sep 30, 2024
63d50f2
21368: getting contact with retry
VSydor Sep 30, 2024
ecc14f6
21368: setting address type
VSydor Sep 30, 2024
28b010e
additional logging
brmeyer Sep 30, 2024
ce016fc
21368: retrying API calls
VSydor Sep 30, 2024
3326299
21368: adding faux contact if account does not have primary contact a…
VSydor Sep 30, 2024
614c003
21368: skipping primary contact person for orgs if does not have emai…
VSydor Sep 30, 2024
400b0b2
use a custom Account ID for organizations
brmeyer Oct 1, 2024
e113bf8
ripping out the old real-time AccountingService
brmeyer Oct 1, 2024
5fdaf45
restrict the donor contact vs. account queries by record type
brmeyer Oct 1, 2024
9d37a50
restrict the donor contact vs. account queries by record type, remove…
brmeyer Oct 1, 2024
77f4a5f
bringing back post invoices using contact UUID (by-account-number did…
brmeyer Oct 2, 2024
4e886e6
query tweak
brmeyer Oct 2, 2024
5e57514
temporarily adding brute force paging to XeroDataSyncService, to be r…
brmeyer Oct 15, 2024
7ea980f
break Xero updateOrCreateTransactions into batches
brmeyer Oct 16, 2024
805b001
error logging for Xero invoice batch creation
brmeyer Oct 23, 2024
1f7ba7f
Xero line items no longer need product codes
brmeyer Oct 24, 2024
e47e016
xero: use the donation name as the line item description as a fallback
brmeyer Oct 25, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/main/java/com/impactupgrade/nucleus/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import com.impactupgrade.nucleus.controller.ScheduledJobController;
import com.impactupgrade.nucleus.controller.SfdcController;
import com.impactupgrade.nucleus.controller.StripeController;
import com.impactupgrade.nucleus.controller.DataSyncController;
import com.impactupgrade.nucleus.controller.TwilioController;
import com.impactupgrade.nucleus.environment.EnvironmentFactory;
import com.impactupgrade.nucleus.security.SecurityExceptionMapper;
Expand Down Expand Up @@ -89,6 +90,7 @@ public void start() throws Exception {

apiConfig.register(backupController());
apiConfig.register(communicationController());
apiConfig.register(dataSyncController());
apiConfig.register(crmController());
apiConfig.register(donationFormController());
apiConfig.register(emailController());
Expand Down Expand Up @@ -145,6 +147,7 @@ public void registerServlets(ServletContextHandler context) throws Exception {}
// Allow orgs to override specific controllers.
protected BackupController backupController() { return new BackupController(envFactory); }
protected CommunicationController communicationController() { return new CommunicationController(envFactory); }
protected DataSyncController dataSyncController() { return new DataSyncController(envFactory); }
protected CrmController crmController() { return new CrmController(envFactory); }
protected DonationFormController donationFormController() { return new DonationFormController(envFactory); }
protected EmailController emailController() { return new EmailController(envFactory); }
Expand Down
69 changes: 68 additions & 1 deletion src/main/java/com/impactupgrade/nucleus/client/SfdcClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public SfdcClient(Environment env, String username, String password, boolean isS
}

if (npsp) {
ACCOUNT_FIELDS += ", npo02__NumberOfClosedOpps__c, npo02__TotalOppAmount__c, npo02__LastCloseDate__c, npo02__LargestAmount__c, npo02__OppsClosedThisYear__c, npo02__OppAmountThisYear__c, npo02__FirstCloseDate__c";
ACCOUNT_FIELDS += ", npo02__NumberOfClosedOpps__c, npo02__TotalOppAmount__c, npo02__LastCloseDate__c, npo02__LargestAmount__c, npo02__OppsClosedThisYear__c, npo02__OppAmountThisYear__c, npo02__FirstCloseDate__c, npe01__One2OneContact__c";
CONTACT_FIELDS += ", account.npo02__NumberOfClosedOpps__c, account.npo02__TotalOppAmount__c, account.npo02__FirstCloseDate__c, account.npo02__LastCloseDate__c, account.npo02__LargestAmount__c, account.npo02__OppsClosedThisYear__c, account.npo02__OppAmountThisYear__c, npe01__Home_Address__c, npe01__WorkPhone__c, npe01__PreferredPhone__c, npe01__HomeEmail__c, npe01__WorkEmail__c, npe01__AlternateEmail__c, npe01__Preferred_Email__c, HomePhone";
DONATION_FIELDS += ", npe03__Recurring_Donation__c";
RECURRINGDONATION_FIELDS = "id, name, npe03__Recurring_Donation_Campaign__c, npe03__Recurring_Donation_Campaign__r.Name, npe03__Next_Payment_Date__c, npe03__Installment_Period__c, npe03__Amount__c, npe03__Open_Ended_Status__c, npe03__Contact__c, npe03__Contact__r.Id, npe03__Contact__r.Name, npe03__Contact__r.Email, npe03__Contact__r.Phone, npe03__Schedule_Type__c, npe03__Date_Established__c, npe03__Organization__c, npe03__Organization__r.Id, npe03__Organization__r.Name, OwnerId, Owner.Id, Owner.IsActive";
Expand Down Expand Up @@ -675,6 +675,64 @@ protected QueryResult querySmsContacts(String updatedSinceClause, String filter,
return query(query);
}

public List<QueryResult> getDonorIndividualContacts(Calendar updatedSince, String... extraFields)
throws ConnectionException, InterruptedException {
List<QueryResult> queryResults = new ArrayList<>();

String updatedSinceClause = "";
if (updatedSince != null) {
updatedSinceClause = "SystemModStamp >= " + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(updatedSince.getTime());
}
queryResults.add(queryDonorIndividualContacts(updatedSinceClause, extraFields));

return queryResults;
}

protected QueryResult queryDonorIndividualContacts(String updatedSinceClause, String... extraFields) throws ConnectionException, InterruptedException {
if (Strings.isNullOrEmpty(updatedSinceClause)) {
env.logJobWarn("no filter provided; out of caution, skipping the query to protect API limits");
return new QueryResult();
}
Set<String> organizationRecordTypeNames = Set.of("business", "church", "school", "org", "group");
String query = "SELECT " + getFieldsList(CONTACT_FIELDS, env.getConfig().salesforce.customQueryFields.contact, extraFields) + " " +
"FROM Contact " +
"WHERE " + updatedSinceClause +
"AND (" + organizationRecordTypeNames.stream()
.map(name -> "Account.RecordType.Name NOT LIKE '%" + name + "%'")
.collect(Collectors.joining(" AND ")) + ") " +
"AND npo02__TotalOppAmount__c > 0.0";
return query(query);
brmeyer marked this conversation as resolved.
Show resolved Hide resolved
}

public List<QueryResult> getDonorOrganizationAccounts(Calendar updatedSince, String... extraFields)
throws ConnectionException, InterruptedException {
List<QueryResult> queryResults = new ArrayList<>();

String updatedSinceClause = "";
if (updatedSince != null) {
updatedSinceClause = "SystemModStamp >= " + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(updatedSince.getTime());
}
queryResults.add(queryDonorOrganizationAccounts(updatedSinceClause, extraFields));

return queryResults;
}

protected QueryResult queryDonorOrganizationAccounts(String updatedSinceClause, String... extraFields) throws ConnectionException, InterruptedException {
if (Strings.isNullOrEmpty(updatedSinceClause)) {
env.logJobWarn("no filter provided; out of caution, skipping the query to protect API limits");
return new QueryResult();
}
Set<String> organizationRecordTypeNames = Set.of("business", "church", "school", "org", "group");
String query = "SELECT " + getFieldsList(ACCOUNT_FIELDS, env.getConfig().salesforce.customQueryFields.account, extraFields) + " " +
"FROM Account " +
"WHERE " + updatedSinceClause +
"AND (" + organizationRecordTypeNames.stream()
.map(name -> "RecordType.Name LIKE '%" + name + "%'")
.collect(Collectors.joining(" OR ")) + ") " +
"AND npo02__TotalOppAmount__c > 0.0";
return query(query);
brmeyer marked this conversation as resolved.
Show resolved Hide resolved
}

public List<SObject> searchContacts(ContactSearch contactSearch, String... extraFields)
throws ConnectionException, InterruptedException {
List<String> clauses = new ArrayList<>();
Expand Down Expand Up @@ -862,6 +920,15 @@ public List<SObject> getDonationsByAccountId(String accountId, String... extraFi
return queryListAutoPaged(query);
}

public List<SObject> getDonationsUpdatedAfter(Calendar updatedSince, String... extraFields) throws ConnectionException, InterruptedException {
// TODO: not allowing updates for now, so only grab what recently closed
// String updatedSinceClause = "SystemModStamp >= " + new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").format(updatedSince.getTime());
String updatedSinceClause = "CloseDate >= " + new SimpleDateFormat("yyyy-MM-dd").format(updatedSince.getTime());
String query = "select " + getFieldsList(DONATION_FIELDS, env.getConfig().salesforce.customQueryFields.donation, extraFields) + " from Opportunity " +
"where " + updatedSinceClause + " AND stageName = 'Closed Won' ORDER BY CloseDate ASC";
return queryListAutoPaged(query);
}

public Optional<SObject> getNextPledgedDonationByRecurringDonationId(String recurringDonationId, String... extraFields) throws ConnectionException, InterruptedException {
// TODO: Using TOMORROW to account for timezone issues -- we can typically get away with that approach
// since most RDs are monthly...
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.impactupgrade.nucleus.controller;

import com.impactupgrade.nucleus.entity.JobStatus;
import com.impactupgrade.nucleus.entity.JobType;
import com.impactupgrade.nucleus.environment.Environment;
import com.impactupgrade.nucleus.environment.EnvironmentFactory;
import com.impactupgrade.nucleus.service.segment.DataSyncService;

import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import java.util.Calendar;

@Path("/data-sync")
public class DataSyncController {

protected final EnvironmentFactory environmentFactory;

public DataSyncController(EnvironmentFactory environmentFactory) {
this.environmentFactory = environmentFactory;
}

@GET
@Path("/contacts/daily")
public Response syncContactsDaily(@QueryParam("syncDays") Integer syncDays, @Context HttpServletRequest request) throws Exception {
Environment env = environmentFactory.init(request);

Calendar lastSync = Calendar.getInstance();
// run daily, but setting this high to catch previous misses
if (syncDays == null || syncDays <= 0) {
syncDays = 3;
}
lastSync.add(Calendar.DATE, -syncDays);

Runnable thread = () -> {
try {
String jobName = "Contacts Sync: Daily";
env.startJobLog(JobType.EVENT, null, jobName, "Nucleus Portal");
boolean success = true;

for (DataSyncService dataSyncService : env.allDataSyncServices()) {
try {
dataSyncService.syncContacts(lastSync);
env.logJobInfo("{}: sync contacts done", dataSyncService.name());
} catch (Exception e) {
env.logJobError("sync contacts failed for {}", dataSyncService.name(), e);
env.logJobError(e.getMessage());
success = false;
}
}

env.endJobLog(success ? JobStatus.DONE : JobStatus.FAILED);

} catch (Exception e) {
env.logJobError("sync contacts failed!", e);
env.logJobError(e.getMessage());
env.endJobLog(JobStatus.FAILED);
}
};
new Thread(thread).start();

return Response.ok().build();
}

@GET
@Path("/transactions/daily")
public Response syncTransactionsDaily(@QueryParam("syncDays") Integer syncDays, @Context HttpServletRequest request) throws Exception {
Environment env = environmentFactory.init(request);

Calendar lastSync = Calendar.getInstance();
// run daily, but setting this high to catch previous misses
if (syncDays == null || syncDays <= 0) {
syncDays = 3;
}
lastSync.add(Calendar.DATE, -syncDays);

Runnable thread = () -> {
try {
String jobName = "Transactions Sync: Daily";
env.startJobLog(JobType.EVENT, null, jobName, "Nucleus Portal");
boolean success = true;

for (DataSyncService dataSyncService : env.allDataSyncServices()) {
try {
dataSyncService.syncTransactions(lastSync);
env.logJobInfo("{}: sync transactions done", dataSyncService.name());
} catch (Exception e) {
env.logJobError("sync transactions failed for {}", dataSyncService.name(), e);
env.logJobError(e.getMessage());
success = false;
}
}

env.endJobLog(success ? JobStatus.DONE : JobStatus.FAILED);

} catch (Exception e) {
env.logJobError("sync transactions failed!", e);
env.logJobError(e.getMessage());
env.endJobLog(JobStatus.FAILED);
}
};
new Thread(thread).start();

return Response.ok().build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ private void processEvent(Event event, Environment env) throws Exception {
//env.donationService().createDonation(paymentGatewayEvent);
env.logJobInfo("Donation created for '" + eventType + "' event.");
env.logJobInfo("Processing transaction for '" + eventType + "' event...");
//env.accountingService().processTransaction(paymentGatewayEvent);
env.logJobInfo("Transaction processed for '" + eventType + "' event.");
}
case "PAYMENT.CAPTURE.DECLINED" -> {
Expand Down Expand Up @@ -170,7 +169,6 @@ private void processEvent(Event event, Environment env) throws Exception {
//env.donationService().createDonation(paymentGatewayEvent);
env.logJobInfo("Donation created for '" + eventType + "' event.");
env.logJobInfo("Processing transaction for '" + eventType + "' event...");
//env.accountingService().processTransaction(paymentGatewayEvent);
env.logJobInfo("Transaction processed for '" + eventType + "' event.");
}
case "BILLING.SUBSCRIPTION.CANCELLED" -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ public void processEvent(String eventType, StripeObject stripeObject, Environmen
// must first process the account/contact so they're available
env.contactService().processDonor(paymentGatewayEvent);
env.donationService().processDonation(paymentGatewayEvent);
env.accountingService().processTransaction(paymentGatewayEvent);
}
}
case "payment_intent.succeeded" -> {
Expand All @@ -142,7 +141,6 @@ public void processEvent(String eventType, StripeObject stripeObject, Environmen
// must first process the account/contact so they're available
env.contactService().processDonor(paymentGatewayEvent);
env.donationService().processDonation(paymentGatewayEvent);
env.accountingService().processTransaction(paymentGatewayEvent);
}
case "charge.failed" -> {
Charge charge = (Charge) stripeObject;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import com.impactupgrade.nucleus.client.VirtuousClient;
import com.impactupgrade.nucleus.entity.JobStatus;
import com.impactupgrade.nucleus.entity.JobType;
import com.impactupgrade.nucleus.service.logic.AccountingService;
import com.impactupgrade.nucleus.service.logic.ActivityService;
import com.impactupgrade.nucleus.service.logic.ContactService;
import com.impactupgrade.nucleus.service.logic.DonationService;
Expand All @@ -32,6 +31,7 @@
import com.impactupgrade.nucleus.service.segment.BareCrmService;
import com.impactupgrade.nucleus.service.segment.PaymentGatewayService;
import com.impactupgrade.nucleus.service.segment.SegmentService;
import com.impactupgrade.nucleus.service.segment.DataSyncService;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections4.map.CaseInsensitiveMap;
import org.apache.http.client.utils.URLEncodedUtils;
Expand Down Expand Up @@ -147,7 +147,6 @@ public void setOtherContext(MultivaluedMap<String, String> otherContext) {
public MessagingService messagingService() { return new MessagingService(this); }
public NotificationService notificationService() { return new NotificationService(this); }
public ScheduledJobService scheduledJobService() { return new ScheduledJobService(this); }
public AccountingService accountingService() { return new AccountingService(this); }

// segment services

Expand Down Expand Up @@ -202,6 +201,14 @@ public CommunicationService communicationService(String name) {
return segmentService(name, CommunicationService.class);
}

public DataSyncService dataSyncService(String name) {
return segmentService(name, DataSyncService.class);
}
brmeyer marked this conversation as resolved.
Show resolved Hide resolved

public List<DataSyncService> allDataSyncServices() {
return segmentServices(DataSyncService.class);
}

public Optional<AccountingPlatformService> accountingPlatformService() {
if (Strings.isNullOrEmpty(getConfig().accountingPrimary)) {
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,6 @@ public static class Salesforce extends Platform {

public Map<AccountType, String> accountTypeToRecordTypeIds = new HashMap<>();
public Map<TransactionType, String> transactionTypeToRecordTypeIds = new HashMap<>();
@Deprecated
public Map<TransactionType, String> paymentEventTypeToRecordTypeIds = transactionTypeToRecordTypeIds;
}
public static class SalesforceCustomFields implements Serializable {
public Set<String> account = new HashSet<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.impactupgrade.nucleus.model;

public class AccountingContact {

public String contactId;
public String crmContactId;

public AccountingContact(String contactId, String crmContactId) {
this.contactId = contactId;
this.crmContactId = crmContactId;
}
}

This file was deleted.

Loading
Loading