├── settings.gradle ├── views ├── Callout.png ├── Thumbdown.png ├── Thumbup.png └── Ratesample.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── main │ ├── java │ │ └── com │ │ │ └── intuit │ │ │ └── developer │ │ │ └── sampleapp │ │ │ └── webhooks │ │ │ ├── service │ │ │ ├── qbo │ │ │ │ ├── QBODataService.java │ │ │ │ ├── WebhooksServiceFactory.java │ │ │ │ ├── CDCService.java │ │ │ │ ├── QueryService.java │ │ │ │ └── DataServiceFactory.java │ │ │ ├── CompanyConfigService.java │ │ │ ├── queue │ │ │ │ ├── QueueService.java │ │ │ │ └── QueueProcessor.java │ │ │ ├── CompanyConfigServiceImpl.java │ │ │ └── security │ │ │ │ └── SecurityService.java │ │ │ ├── Application.java │ │ │ ├── domain │ │ │ ├── ResponseWrapper.java │ │ │ ├── AppConfig.java │ │ │ └── CompanyConfig.java │ │ │ ├── repository │ │ │ ├── CompanyConfigRepository.java │ │ │ └── PersistenceConfiguration.java │ │ │ ├── controllers │ │ │ ├── CompanyController.java │ │ │ └── WebhooksController.java │ │ │ └── DataLoader.java │ └── resources │ │ └── application.properties └── test │ └── java │ └── com │ └── intuit │ └── developer │ └── sampleapp │ └── webhooks │ ├── service │ ├── SecurityServiceTest.java │ ├── DataServiceFactoryTest.java │ └── CDCServiceTest.java │ └── controller │ └── WebhooksControllerTest.java ├── NOTICE.txt ├── gradlew.bat ├── gradlew └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | 2 | rootProject.name = 'SampleApp-Webhooks-Java' 3 | -------------------------------------------------------------------------------- /views/Callout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-Webhooks-Java/HEAD/views/Callout.png -------------------------------------------------------------------------------- /views/Thumbdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-Webhooks-Java/HEAD/views/Thumbdown.png -------------------------------------------------------------------------------- /views/Thumbup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-Webhooks-Java/HEAD/views/Thumbup.png -------------------------------------------------------------------------------- /views/Ratesample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-Webhooks-Java/HEAD/views/Ratesample.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/IntuitDeveloper/SampleApp-Webhooks-Java/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Jun 21 15:34:32 PDT 2016 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.9-bin.zip 7 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/service/qbo/QBODataService.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service.qbo; 2 | 3 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 4 | 5 | /** 6 | * Interface holding methods to call QBP Dataservice api 7 | * 8 | * @author dderose 9 | * 10 | */ 11 | public interface QBODataService { 12 | 13 | public void callDataService(CompanyConfig companyConfig) throws Exception; 14 | 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/Application.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * @author dderose 8 | * 9 | */ 10 | @SpringBootApplication 11 | public class Application { 12 | 13 | public static void main(String[] args) { 14 | SpringApplication.run(Application.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/domain/ResponseWrapper.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.domain; 2 | 3 | /** 4 | * @author dderose 5 | * 6 | */ 7 | public class ResponseWrapper { 8 | 9 | private String message; 10 | 11 | public ResponseWrapper(String message) { 12 | this.message = message; 13 | } 14 | 15 | public String getMessage() { 16 | return message; 17 | } 18 | 19 | public void setMessage(String message) { 20 | this.message = message; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /NOTICE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 Intuit, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at 4 | 5 | http://www.apache.org/licenses/LICENSE-2.0 6 | 7 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 8 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/repository/CompanyConfigRepository.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.repository; 2 | 3 | import org.springframework.data.repository.CrudRepository; 4 | import org.springframework.data.repository.query.Param; 5 | 6 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 7 | 8 | /** 9 | * @author dderose 10 | * 11 | */ 12 | public interface CompanyConfigRepository extends CrudRepository { 13 | 14 | CompanyConfig findByRealmId(@Param("realmId") String realmId); 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/service/CompanyConfigService.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service; 2 | 3 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 4 | 5 | /** 6 | * @author dderose 7 | * 8 | */ 9 | public interface CompanyConfigService { 10 | 11 | public Iterable getAllCompanyConfigs(); 12 | 13 | public CompanyConfig getCompanyConfigByRealmId(String realmId); 14 | 15 | public CompanyConfig getCompanyConfigById(Integer id); 16 | 17 | public void save(CompanyConfig companyConfig); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/service/qbo/WebhooksServiceFactory.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service.qbo; 2 | 3 | import org.springframework.stereotype.Service; 4 | 5 | import com.intuit.ipp.services.WebhooksService; 6 | 7 | /** 8 | * 9 | * @author dderose 10 | * 11 | */ 12 | @Service 13 | public class WebhooksServiceFactory { 14 | 15 | /** 16 | * Initializes WebhooksService 17 | * 18 | * @return 19 | */ 20 | public WebhooksService getWebhooksService() { 21 | 22 | //create WebhooksService 23 | return new WebhooksService(); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/repository/PersistenceConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.repository; 2 | 3 | import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 4 | import org.springframework.boot.orm.jpa.EntityScan; 5 | import org.springframework.context.annotation.Configuration; 6 | import org.springframework.data.jpa.repository.config.EnableJpaRepositories; 7 | import org.springframework.transaction.annotation.EnableTransactionManagement; 8 | 9 | /** 10 | * @author dderose 11 | * 12 | */ 13 | @Configuration 14 | @EnableAutoConfiguration 15 | @EntityScan(basePackages = {"com.intuit.developer.sampleapp.webhooks.domain"}) 16 | @EnableJpaRepositories(basePackages = {"com.intuit.developer.sampleapp.webhooks.repository"}) 17 | @EnableTransactionManagement 18 | public class PersistenceConfiguration { 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #For OAuth2 app set oauth.type=2 2 | oauth.type=1 3 | 4 | #sandbox app config - needed only for Oauth1 apps 5 | consumer.key= 6 | consumer.secret= 7 | app.token= 8 | 9 | #sandbox company config 10 | company1.id= 11 | company1.accessToken= 12 | company1.accessTokenSecret= 13 | #add more entities if required 14 | company1.webhooks.subscribed.entities=Customer,Vendor,Invoice 15 | #OAuth2 access token 16 | company1.oauth2.accessToken= 17 | 18 | #if you want to test with only one company, comment the next 5 lines below and line 77-78 in DataLoader.java 19 | company2.id= 20 | company2.accessToken= 21 | company2.accessTokenSecret= 22 | company2.webhooks.subscribed.entities=Customer,Vendor,Employee 23 | #OAuth2 access token 24 | company2.oauth2.accessToken= 25 | 26 | #sandbox verifier token 27 | webhooks.verifier.token= 28 | 29 | #QBO Sandbox url 30 | qbo.url=https://sandbox-quickbooks.api.intuit.com/v3/company 31 | 32 | #encryption key 33 | encryption.key=0123456789abcdef 34 | 35 | # enable this to change the port 36 | #server.port = 8090 -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/domain/AppConfig.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.domain; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.context.annotation.PropertySource; 6 | import org.springframework.core.env.Environment; 7 | 8 | /** 9 | * Entity to store app configs 10 | * 11 | * @author dderose 12 | * 13 | */ 14 | @Configuration 15 | @PropertySource(value="classpath:/application.properties", ignoreResourceNotFound=true) 16 | public class AppConfig { 17 | 18 | @Autowired 19 | Environment env; 20 | 21 | public String getAppToken() { 22 | return env.getProperty("app.token"); 23 | } 24 | 25 | public String getConsumerKey() { 26 | return env.getProperty("consumer.key"); 27 | } 28 | 29 | public String getConsumerSecret() { 30 | return env.getProperty("consumer.secret"); 31 | } 32 | 33 | public String getQboUrl() { 34 | return env.getProperty("qbo.url"); 35 | } 36 | 37 | //Flag to determine if app is OAuth1 or 2 38 | public String getOAuthType() { 39 | return env.getProperty("oauth.type"); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/controllers/CompanyController.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.controllers; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.RequestMapping; 7 | import org.springframework.web.bind.annotation.RequestMethod; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 11 | import com.intuit.developer.sampleapp.webhooks.service.CompanyConfigService; 12 | 13 | /** 14 | * Controller class for company config enpoints 15 | * @author dderose 16 | * 17 | */ 18 | @RestController 19 | public class CompanyController { 20 | 21 | @Autowired 22 | private CompanyConfigService companyConfigService; 23 | 24 | @RequestMapping(value = "/companyConfigs", method = RequestMethod.GET) 25 | public ResponseEntity> listAllRealmConfigs() { 26 | 27 | Iterable log = companyConfigService.getAllCompanyConfigs(); 28 | 29 | return new ResponseEntity<>(log, HttpStatus.OK); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/service/queue/QueueService.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service.queue; 2 | 3 | import java.util.concurrent.BlockingQueue; 4 | import java.util.concurrent.ExecutorService; 5 | import java.util.concurrent.Executors; 6 | import java.util.concurrent.LinkedBlockingQueue; 7 | 8 | import javax.annotation.PostConstruct; 9 | import javax.annotation.PreDestroy; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Service; 13 | 14 | import com.intuit.ipp.util.Logger; 15 | 16 | /** 17 | * Manages a queue and executes a single async thread to process the queue whenever an item is added to the queue 18 | * @author dderose 19 | * 20 | */ 21 | @Service 22 | public class QueueService { 23 | 24 | private static final org.slf4j.Logger LOG = Logger.getLogger(); 25 | 26 | private static final BlockingQueue QUEUE = new LinkedBlockingQueue<>(); 27 | 28 | @Autowired 29 | private QueueProcessor queueProcessor; 30 | 31 | private ExecutorService executorService; 32 | 33 | @PostConstruct 34 | public void init() { 35 | // intitialize a single thread executor, this will ensure only one thread processes the queue 36 | executorService = Executors.newSingleThreadExecutor(); 37 | } 38 | 39 | public void add(String payload) { 40 | 41 | // add payload to the queue 42 | QUEUE.add(payload); 43 | LOG.info("added to queue:::: queue size " + QUEUE.size()); 44 | 45 | //Call executor service 46 | executorService.submit(queueProcessor); 47 | } 48 | 49 | public BlockingQueue getQueue() { 50 | return QUEUE; 51 | } 52 | 53 | @PreDestroy 54 | public void shutdown() { 55 | executorService.shutdown(); 56 | } 57 | 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/service/qbo/CDCService.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service.qbo; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Arrays; 5 | import java.util.List; 6 | 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Service; 9 | 10 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 11 | import com.intuit.ipp.core.IEntity; 12 | import com.intuit.ipp.services.DataService; 13 | import com.intuit.ipp.util.Logger; 14 | 15 | /** 16 | * Class for implementing the QBO CDC api 17 | * 18 | * @author dderose 19 | * 20 | */ 21 | @Service(value="CdcAPI") 22 | public class CDCService implements QBODataService { 23 | 24 | private static final org.slf4j.Logger LOG = Logger.getLogger(); 25 | 26 | @Autowired 27 | DataServiceFactory dataServiceFactory; 28 | 29 | @Override 30 | public void callDataService(CompanyConfig companyConfig) throws Exception { 31 | 32 | // create data service 33 | DataService service = dataServiceFactory.getDataService(companyConfig); 34 | 35 | try { 36 | LOG.info("Calling CDC "); 37 | // build entity list for cdc based on entities subscribed for webhooks 38 | List subscribedEntities = Arrays.asList(companyConfig.getWebhooksSubscribedEntites().split(",")); 39 | List entities = new ArrayList<>(); 40 | for (String subscribedEntity : subscribedEntities) { 41 | Class className = Class.forName("com.intuit.ipp.data." + subscribedEntity); 42 | IEntity entity = (IEntity) className.newInstance(); 43 | entities.add(entity); 44 | } 45 | // call CDC 46 | service.executeCDCQuery(entities, companyConfig.getLastCdcTimestamp()); 47 | LOG.info("CDC complete "); 48 | 49 | } catch (Exception ex) { 50 | LOG.error("Error while calling CDC" , ex.getCause()); 51 | } 52 | 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/service/qbo/QueryService.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service.qbo; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Service; 8 | 9 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 10 | import com.intuit.ipp.services.DataService; 11 | import com.intuit.ipp.util.Logger; 12 | 13 | /** 14 | * Class for implementing QBO Query api 15 | * 16 | * @author dderose 17 | * 18 | */ 19 | @Service(value="QueryAPI") 20 | public class QueryService implements QBODataService { 21 | 22 | private static final org.slf4j.Logger LOG = Logger.getLogger(); 23 | 24 | @Autowired 25 | DataServiceFactory dataServiceFactory; 26 | 27 | @Override 28 | public void callDataService(CompanyConfig companyConfig) throws Exception { 29 | 30 | // create data service 31 | DataService service = dataServiceFactory.getDataService(companyConfig); 32 | 33 | try { 34 | LOG.info("Calling Query API "); 35 | String query = "select * from "; 36 | //Build query list for each subscribed entities 37 | List subscribedEntities = Arrays.asList(companyConfig.getWebhooksSubscribedEntites().split(",")); 38 | subscribedEntities.forEach(entity -> executeQuery(query + entity, service)) ; 39 | 40 | } catch (Exception ex) { 41 | LOG.error("Error loading app configs" , ex.getCause()); 42 | } 43 | 44 | } 45 | 46 | /** 47 | * Call executeQuery api for each entity 48 | * 49 | * @param query 50 | * @param service 51 | */ 52 | public void executeQuery(String query, DataService service) { 53 | try { 54 | LOG.info("Executing Query " + query); 55 | service.executeQuery(query); 56 | LOG.info(" Query complete" ); 57 | } catch (Exception ex) { 58 | LOG.error("Error loading app configs" , ex.getCause()); 59 | } 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/service/qbo/DataServiceFactory.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service.qbo; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | 6 | import com.intuit.developer.sampleapp.webhooks.domain.AppConfig; 7 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 8 | import com.intuit.ipp.core.Context; 9 | import com.intuit.ipp.core.ServiceType; 10 | import com.intuit.ipp.exception.FMSException; 11 | import com.intuit.ipp.security.IAuthorizer; 12 | import com.intuit.ipp.security.OAuth2Authorizer; 13 | import com.intuit.ipp.security.OAuthAuthorizer; 14 | import com.intuit.ipp.services.DataService; 15 | import com.intuit.ipp.util.Config; 16 | 17 | /** 18 | * 19 | * @author dderose 20 | * 21 | */ 22 | @Service 23 | public class DataServiceFactory { 24 | 25 | @Autowired 26 | AppConfig appConfig; 27 | 28 | /** 29 | * Initializes DataService for a given app/company profile 30 | * 31 | * @param companyConfig 32 | * @return 33 | * @throws FMSException 34 | */ 35 | public DataService getDataService(CompanyConfig companyConfig) throws FMSException { 36 | 37 | //set custom config, this should be commented for prod 38 | Config.setProperty(Config.BASE_URL_QBO, appConfig.getQboUrl()); 39 | 40 | //create oauth object based on OAuth type 41 | IAuthorizer oauth; 42 | if(appConfig.getOAuthType().equals("1")) { 43 | oauth = new OAuthAuthorizer(appConfig.getConsumerKey(), appConfig.getConsumerSecret(), companyConfig.getAccessToken(), companyConfig.getAccessTokenSecret()); 44 | } else { 45 | oauth = new OAuth2Authorizer(companyConfig.getOauth2BearerToken()); 46 | } 47 | //create context 48 | Context context = new Context(oauth, appConfig.getAppToken(), ServiceType.QBO, companyConfig.getRealmId()); 49 | 50 | //create dataservice 51 | return new DataService(context); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/developer/sampleapp/webhooks/service/SecurityServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.mockito.Mockito; 8 | import org.mockito.MockitoAnnotations; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.core.env.Environment; 11 | import org.springframework.test.context.ContextConfiguration; 12 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 13 | import org.springframework.test.context.web.AnnotationConfigWebContextLoader; 14 | import org.springframework.test.context.web.WebAppConfiguration; 15 | import org.springframework.test.util.ReflectionTestUtils; 16 | import org.springframework.web.context.WebApplicationContext; 17 | 18 | import com.intuit.developer.sampleapp.webhooks.service.qbo.WebhooksServiceFactory; 19 | import com.intuit.developer.sampleapp.webhooks.service.security.SecurityService; 20 | import com.intuit.ipp.services.WebhooksService; 21 | 22 | /** 23 | * @author dderose 24 | * 25 | */ 26 | @RunWith(SpringJUnit4ClassRunner.class) 27 | @WebAppConfiguration 28 | @ContextConfiguration(loader=AnnotationConfigWebContextLoader.class) 29 | public class SecurityServiceTest { 30 | 31 | SecurityService securityService = new SecurityService(); 32 | 33 | @Autowired 34 | protected WebApplicationContext wac; 35 | 36 | Environment env; 37 | WebhooksServiceFactory webhooksServiceFactory; 38 | WebhooksService webhooksService; 39 | 40 | @Before 41 | public void setUp() throws Exception { 42 | MockitoAnnotations.initMocks(this); 43 | env = Mockito.mock(Environment.class); 44 | 45 | final String key = "12345"; 46 | Mockito.when(env.getProperty("webhooks.verifier.token")).thenReturn(key); 47 | Mockito.when(env.getProperty("encryption.key")).thenReturn(key); 48 | 49 | ReflectionTestUtils.setField(securityService, "env", env); 50 | webhooksServiceFactory = Mockito.mock(WebhooksServiceFactory.class); 51 | ReflectionTestUtils.setField(securityService, "webhooksServiceFactory", webhooksServiceFactory); 52 | webhooksService = Mockito.mock(WebhooksService.class); 53 | Mockito.when(webhooksServiceFactory.getWebhooksService()).thenReturn(webhooksService); 54 | } 55 | 56 | @Test 57 | public void testValidRequest() { 58 | 59 | boolean result = securityService.isRequestValid("12345", "abcd"); 60 | Assert.assertFalse(result); 61 | 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/controllers/WebhooksController.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.controllers; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.http.HttpStatus; 5 | import org.springframework.http.ResponseEntity; 6 | import org.springframework.web.bind.annotation.RequestBody; 7 | import org.springframework.web.bind.annotation.RequestHeader; 8 | import org.springframework.web.bind.annotation.RequestMapping; 9 | import org.springframework.web.bind.annotation.RequestMethod; 10 | import org.springframework.web.bind.annotation.ResponseBody; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | import com.intuit.developer.sampleapp.webhooks.domain.ResponseWrapper; 14 | import com.intuit.developer.sampleapp.webhooks.service.queue.QueueService; 15 | import com.intuit.developer.sampleapp.webhooks.service.security.SecurityService; 16 | import com.intuit.ipp.util.Logger; 17 | import com.intuit.ipp.util.StringUtils; 18 | 19 | /** 20 | * Controller class for the webhooks endpoint 21 | * 22 | * @author dderose 23 | * 24 | */ 25 | @RestController 26 | public class WebhooksController { 27 | 28 | private static final org.slf4j.Logger LOG = Logger.getLogger(); 29 | 30 | private static final String SIGNATURE = "intuit-signature"; 31 | private static final String SUCCESS = "Success"; 32 | private static final String ERROR = "Error"; 33 | 34 | @Autowired 35 | SecurityService securityService; 36 | 37 | @Autowired 38 | private QueueService queueService; 39 | 40 | /** 41 | * Method to receive webhooks event notification 42 | * 1. Validates payload 43 | * 2. Adds it to a queue 44 | * 3. Sends success response back 45 | * 46 | * Note: Queue processing occurs in an async thread 47 | * 48 | * @param signature 49 | * @param payload 50 | * @return 51 | */ 52 | @RequestMapping(value = "/webhooks", method = RequestMethod.POST) 53 | public ResponseEntity webhooks(@RequestHeader(SIGNATURE) String signature, @RequestBody String payload) { 54 | 55 | // if signature is empty return 401 56 | if (!StringUtils.hasText(signature)) { 57 | return new ResponseEntity<>(new ResponseWrapper(ERROR), HttpStatus.FORBIDDEN); 58 | } 59 | 60 | // if payload is empty, don't do anything 61 | if (!StringUtils.hasText(payload)) { 62 | new ResponseEntity<>(new ResponseWrapper(SUCCESS), HttpStatus.OK); 63 | } 64 | 65 | LOG.info("request recieved "); 66 | 67 | //if request valid - push to queue 68 | if (securityService.isRequestValid(signature, payload)) { 69 | queueService.add(payload); 70 | } else { 71 | return new ResponseEntity<>(new ResponseWrapper(ERROR), HttpStatus.FORBIDDEN); 72 | } 73 | 74 | LOG.info("response sent "); 75 | return new ResponseEntity<>(new ResponseWrapper(SUCCESS), HttpStatus.OK); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/developer/sampleapp/webhooks/service/DataServiceFactoryTest.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service; 2 | 3 | import org.junit.Assert; 4 | import org.junit.Before; 5 | import org.junit.Test; 6 | import org.junit.runner.RunWith; 7 | import org.mockito.Mockito; 8 | import org.mockito.MockitoAnnotations; 9 | import org.springframework.test.context.ContextConfiguration; 10 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 11 | import org.springframework.test.context.web.AnnotationConfigWebContextLoader; 12 | import org.springframework.test.context.web.WebAppConfiguration; 13 | import org.springframework.test.util.ReflectionTestUtils; 14 | 15 | import com.intuit.developer.sampleapp.webhooks.domain.AppConfig; 16 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 17 | import com.intuit.developer.sampleapp.webhooks.service.qbo.DataServiceFactory; 18 | import com.intuit.ipp.core.Context; 19 | import com.intuit.ipp.security.OAuthAuthorizer; 20 | import com.intuit.ipp.services.DataService; 21 | 22 | /** 23 | * @author dderose 24 | * 25 | */ 26 | @RunWith(SpringJUnit4ClassRunner.class) 27 | @WebAppConfiguration 28 | @ContextConfiguration(loader=AnnotationConfigWebContextLoader.class) 29 | public class DataServiceFactoryTest { 30 | 31 | DataServiceFactory dataServiceFactory; 32 | DataService dataService; 33 | OAuthAuthorizer oAuthAuthorizer; 34 | Context context; 35 | AppConfig appConfig; 36 | 37 | @Before 38 | public void setUp() throws Exception { 39 | MockitoAnnotations.initMocks(this); 40 | dataServiceFactory = new DataServiceFactory(); 41 | dataService = Mockito.mock(DataService.class); 42 | oAuthAuthorizer = Mockito.mock(OAuthAuthorizer.class); 43 | context = Mockito.mock(Context.class); 44 | appConfig = Mockito.mock(AppConfig.class); 45 | 46 | final String consumerKey = "consumerKey"; 47 | final String consumerSecret = "consumerSecret"; 48 | final String appToken = "appToken"; 49 | final String qboUrl = "qbourl"; 50 | 51 | Mockito.when(appConfig.getConsumerKey()).thenReturn(consumerKey); 52 | Mockito.when(appConfig.getConsumerSecret()).thenReturn(consumerSecret); 53 | Mockito.when(appConfig.getAppToken()).thenReturn(appToken); 54 | Mockito.when(appConfig.getQboUrl()).thenReturn(qboUrl); 55 | Mockito.when(appConfig.getOAuthType()).thenReturn("1"); 56 | ReflectionTestUtils.setField(dataServiceFactory, "appConfig", appConfig); 57 | } 58 | 59 | @Test 60 | public void testGetDataService() throws Exception { 61 | final String accessToken = "accessToken"; 62 | final String accessTokenSecret = "accessTokenSecret"; 63 | final String realmId = "1234567"; 64 | 65 | CompanyConfig c = new CompanyConfig(realmId, accessToken, accessTokenSecret, null, null); 66 | 67 | DataService ds = dataServiceFactory.getDataService(c); 68 | Assert.assertNotNull(ds); 69 | } 70 | 71 | 72 | 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/domain/CompanyConfig.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.domain; 2 | 3 | import javax.persistence.Column; 4 | import javax.persistence.Entity; 5 | import javax.persistence.GeneratedValue; 6 | import javax.persistence.GenerationType; 7 | import javax.persistence.Id; 8 | 9 | /** 10 | * Entity to store oauth tokens and other configs for the QBO company 11 | * 12 | * @author dderose 13 | * 14 | */ 15 | @Entity 16 | public class CompanyConfig { 17 | 18 | @Id 19 | @GeneratedValue(strategy = GenerationType.AUTO) 20 | private Integer id; 21 | 22 | private String realmId; //companyId 23 | 24 | private String accessToken; 25 | 26 | private String accessTokenSecret; 27 | 28 | private String webhooksSubscribedEntites; // this will be a comma separated list of entities 29 | 30 | private String lastCdcTimestamp; // timestamp when the last CDC call was made 31 | 32 | @Column(length = 1000) 33 | private String oauth2BearerToken; //for OAuth2 apps set this, accesstoken and accessTokenSecret will not be available. 34 | 35 | public CompanyConfig(String realmId, String accessToken, String accessTokenSecret, String webhooksSubscribedEntites, String oauth2BearerToken) { 36 | this.realmId = realmId; 37 | this.accessToken = accessToken; 38 | this.accessTokenSecret = accessTokenSecret; 39 | this.webhooksSubscribedEntites = webhooksSubscribedEntites; 40 | this.oauth2BearerToken = oauth2BearerToken; 41 | } 42 | 43 | public CompanyConfig() { 44 | 45 | } 46 | 47 | public Integer getId() { 48 | return id; 49 | } 50 | 51 | public void setId(Integer id) { 52 | this.id = id; 53 | } 54 | 55 | public String getRealmId() { 56 | return realmId; 57 | } 58 | 59 | public void setRealmId(String realmId) { 60 | this.realmId = realmId; 61 | } 62 | 63 | public String getAccessToken() { 64 | return accessToken; 65 | } 66 | 67 | public void setAccessToken(String accessToken) { 68 | this.accessToken = accessToken; 69 | } 70 | 71 | public String getAccessTokenSecret() { 72 | return accessTokenSecret; 73 | } 74 | 75 | public void setAccessTokenSecret(String accessTokenSecret) { 76 | this.accessTokenSecret = accessTokenSecret; 77 | } 78 | 79 | public String getWebhooksSubscribedEntites() { 80 | return webhooksSubscribedEntites; 81 | } 82 | 83 | public void setWebhooksSubscribedEntites(String webhooksSubscribedEntites) { 84 | this.webhooksSubscribedEntites = webhooksSubscribedEntites; 85 | } 86 | 87 | public String getLastCdcTimestamp() { 88 | return lastCdcTimestamp; 89 | } 90 | 91 | public void setLastCdcTimestamp(String lastCdcTimestamp) { 92 | this.lastCdcTimestamp = lastCdcTimestamp; 93 | } 94 | 95 | public String getOauth2BearerToken() { 96 | return oauth2BearerToken; 97 | } 98 | 99 | public void setOauth2BearerToken(String oauth2BearerToken) { 100 | this.oauth2BearerToken = oauth2BearerToken; 101 | } 102 | 103 | 104 | } 105 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/service/queue/QueueProcessor.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service.queue; 2 | 3 | import java.util.concurrent.Callable; 4 | 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.beans.factory.annotation.Qualifier; 7 | import org.springframework.stereotype.Service; 8 | 9 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 10 | import com.intuit.developer.sampleapp.webhooks.service.CompanyConfigService; 11 | import com.intuit.developer.sampleapp.webhooks.service.qbo.QBODataService; 12 | import com.intuit.developer.sampleapp.webhooks.service.qbo.WebhooksServiceFactory; 13 | import com.intuit.ipp.data.EventNotification; 14 | import com.intuit.ipp.data.WebhooksEvent; 15 | import com.intuit.ipp.services.WebhooksService; 16 | import com.intuit.ipp.util.DateUtils; 17 | import com.intuit.ipp.util.Logger; 18 | 19 | 20 | /** 21 | * Callable task to process the queue 22 | * 1. Retrieves the payload from the queue 23 | * 2. Converts json to object 24 | * 3. Queries CompanyConfig table to get the last CDC performed time for the realmId 25 | * 4. Performs CDC for all the subscribed entities using the lastCDCTime retrieved in step 3 26 | * 5. Updates the CompanyConfig table with the last CDC performed time for the realmId - time when step 4 was performed 27 | * 28 | * @author dderose 29 | * 30 | */ 31 | @Service 32 | public class QueueProcessor implements Callable { 33 | 34 | private static final org.slf4j.Logger LOG = Logger.getLogger(); 35 | 36 | @Autowired 37 | @Qualifier("CdcAPI") 38 | private QBODataService cdcService; 39 | 40 | @Autowired 41 | private QueueService queueService; 42 | 43 | @Autowired 44 | private CompanyConfigService companyConfigService; 45 | 46 | @Autowired 47 | WebhooksServiceFactory webhooksServiceFactory; 48 | 49 | public static final String DATE_yyyyMMddTHHmmssSSSZ = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"; 50 | 51 | @Override 52 | public Object call() throws Exception { 53 | 54 | while (!queueService.getQueue().isEmpty()) { 55 | //remove item from queue 56 | String payload = queueService.getQueue().poll(); 57 | LOG.info("processing payload: Queue Size:" + queueService.getQueue().size()); 58 | 59 | // create webhooks service 60 | WebhooksService service = webhooksServiceFactory.getWebhooksService(); 61 | 62 | //Convert payload to obj 63 | WebhooksEvent event = service.getWebhooksEvent(payload); 64 | for (EventNotification eventNotification : event.getEventNotifications()) { 65 | 66 | // get the company config 67 | CompanyConfig companyConfig = companyConfigService.getCompanyConfigByRealmId(eventNotification.getRealmId()); 68 | 69 | // perform cdc with last updated timestamp and subscribed entities 70 | String cdcTimestamp = DateUtils.getStringFromDateTime(DateUtils.getCurrentDateTime()); 71 | cdcService.callDataService(companyConfig); 72 | 73 | // update cdcTimestamp in companyconfig 74 | companyConfig.setLastCdcTimestamp(cdcTimestamp); 75 | companyConfigService.save(companyConfig); 76 | } 77 | } 78 | 79 | return null; 80 | } 81 | 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/DataLoader.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.beans.factory.annotation.Qualifier; 5 | import org.springframework.context.ApplicationListener; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.context.annotation.PropertySource; 8 | import org.springframework.context.event.ContextRefreshedEvent; 9 | import org.springframework.core.env.Environment; 10 | import org.springframework.stereotype.Component; 11 | 12 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 13 | import com.intuit.developer.sampleapp.webhooks.service.CompanyConfigService; 14 | import com.intuit.developer.sampleapp.webhooks.service.qbo.QBODataService; 15 | import com.intuit.ipp.util.DateUtils; 16 | import com.intuit.ipp.util.Logger; 17 | 18 | /** 19 | * @author dderose 20 | * 21 | */ 22 | @Component 23 | @Configuration 24 | @PropertySource(value="classpath:/application.properties", ignoreResourceNotFound=true) 25 | public class DataLoader implements ApplicationListener { 26 | 27 | private static final org.slf4j.Logger LOG = Logger.getLogger(); 28 | 29 | @Autowired 30 | private Environment env; 31 | 32 | @Autowired 33 | private CompanyConfigService companyConfigService; 34 | 35 | @Autowired 36 | @Qualifier("QueryAPI") 37 | private QBODataService queryService; 38 | 39 | @Override 40 | public void onApplicationEvent(ContextRefreshedEvent event) { 41 | 42 | // Load CompanyConfig table with realmIds and access tokens 43 | loadCompanyConfig(); 44 | 45 | //get list of companyConfigs 46 | Iterable companyConfigs = companyConfigService.getAllCompanyConfigs(); 47 | 48 | //run findQuery for all entities for each realmId 49 | // and update the timestamp in the database 50 | // This is done so that the app is synced before it listens to event notifications 51 | for (CompanyConfig config : companyConfigs) { 52 | try { 53 | String lastQueryTimestamp = DateUtils.getStringFromDateTime(DateUtils.getCurrentDateTime()); 54 | queryService.callDataService(config); 55 | 56 | //update timestamp data in table 57 | config.setLastCdcTimestamp(lastQueryTimestamp); 58 | companyConfigService.save(config); 59 | 60 | } catch (Exception ex) { 61 | LOG.error("Error loading company configs" , ex.getCause()); 62 | } 63 | } 64 | 65 | } 66 | 67 | /** 68 | * Read access tokens and other properties from the configuration file and load it in the in-memory h2 database 69 | * 70 | */ 71 | private void loadCompanyConfig() { 72 | final CompanyConfig companyConfig = new CompanyConfig(env.getProperty("company1.id"), env.getProperty("company1.accessToken"), env.getProperty("company1.accessTokenSecret"), env.getProperty("company1.webhooks.subscribed.entities"), env.getProperty("company1.oauth2.accessToken")); 73 | companyConfigService.save(companyConfig); 74 | 75 | final CompanyConfig company2 = new CompanyConfig(env.getProperty("company2.id"), env.getProperty("company2.accessToken"), env.getProperty("company2.accessTokenSecret"), env.getProperty("company2.webhooks.subscribed.entities"), env.getProperty("company2.oauth2.accessToken")); 76 | companyConfigService.save(company2); 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/service/CompanyConfigServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service; 2 | 3 | import org.springframework.beans.factory.annotation.Autowired; 4 | import org.springframework.stereotype.Service; 5 | 6 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 7 | import com.intuit.developer.sampleapp.webhooks.repository.CompanyConfigRepository; 8 | import com.intuit.developer.sampleapp.webhooks.service.security.SecurityService; 9 | import com.intuit.ipp.util.Logger; 10 | 11 | /** 12 | * Service class to store and retrieve CompanyConfig data from database 13 | * During save access token and secret is encrypted 14 | * During retrieve the tokens are decrypted 15 | * 16 | * @author dderose 17 | * 18 | */ 19 | @Service 20 | public class CompanyConfigServiceImpl implements CompanyConfigService { 21 | 22 | private static final org.slf4j.Logger LOG = Logger.getLogger(); 23 | 24 | @Autowired 25 | private CompanyConfigRepository companyConfigRepository; 26 | 27 | @Autowired 28 | private SecurityService securityService; 29 | 30 | @Override 31 | public Iterable getAllCompanyConfigs() { 32 | Iterable companyConfigs = companyConfigRepository.findAll(); 33 | try { 34 | companyConfigs.forEach(config -> config.setAccessToken(decrypt(config.getAccessToken()))); 35 | companyConfigs.forEach(config -> config.setAccessTokenSecret(decrypt(config.getAccessTokenSecret()))); 36 | } catch (Exception ex) { 37 | LOG.error("Error loading company configs" , ex.getCause()); 38 | } 39 | return companyConfigs; 40 | } 41 | 42 | @Override 43 | public CompanyConfig getCompanyConfigByRealmId(String realmId) { 44 | CompanyConfig companyConfig = companyConfigRepository.findByRealmId(realmId); 45 | try { 46 | companyConfig.setAccessToken(decrypt(companyConfig.getAccessToken())); 47 | companyConfig.setAccessTokenSecret(decrypt(companyConfig.getAccessTokenSecret())); 48 | return companyConfig; 49 | } catch (Exception ex) { 50 | LOG.error("Error loading company config" , ex.getCause()); 51 | return null; 52 | } 53 | 54 | } 55 | 56 | @Override 57 | public CompanyConfig getCompanyConfigById(Integer id) { 58 | CompanyConfig companyConfig = companyConfigRepository.findOne(id); 59 | try { 60 | companyConfig.setAccessToken(decrypt(companyConfig.getAccessToken())); 61 | companyConfig.setAccessTokenSecret(decrypt(companyConfig.getAccessTokenSecret())); 62 | return companyConfig; 63 | } catch (Exception ex) { 64 | LOG.error("Error loading company config" , ex.getCause()); 65 | return null; 66 | } 67 | 68 | } 69 | 70 | @Override 71 | public void save(CompanyConfig companyConfig) { 72 | try { 73 | companyConfig.setAccessToken(encrypt(companyConfig.getAccessToken())); 74 | companyConfig.setAccessTokenSecret(encrypt(companyConfig.getAccessTokenSecret())); 75 | companyConfigRepository.save(companyConfig); 76 | } catch (Exception ex) { 77 | LOG.error("Error loading company config" , ex.getCause()); 78 | } 79 | 80 | } 81 | 82 | public String decrypt(String string) { 83 | try { 84 | return securityService.decrypt(string); 85 | } catch (Exception ex) { 86 | LOG.error("Error decrypting" , ex.getCause()); 87 | return null; 88 | } 89 | } 90 | 91 | public String encrypt(String string) { 92 | try { 93 | return securityService.encrypt(string); 94 | } catch (Exception ex) { 95 | LOG.error("Error encrypting" , ex.getCause()); 96 | return null; 97 | } 98 | } 99 | 100 | } 101 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/developer/sampleapp/webhooks/service/CDCServiceTest.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service; 2 | 3 | import org.junit.Before; 4 | import org.junit.Test; 5 | import org.junit.runner.RunWith; 6 | import org.mockito.Mockito; 7 | import org.mockito.MockitoAnnotations; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.test.context.ContextConfiguration; 10 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 11 | import org.springframework.test.context.web.AnnotationConfigWebContextLoader; 12 | import org.springframework.test.context.web.WebAppConfiguration; 13 | import org.springframework.test.util.ReflectionTestUtils; 14 | import org.springframework.web.context.WebApplicationContext; 15 | 16 | import com.intuit.developer.sampleapp.webhooks.domain.AppConfig; 17 | import com.intuit.developer.sampleapp.webhooks.domain.CompanyConfig; 18 | import com.intuit.developer.sampleapp.webhooks.service.qbo.CDCService; 19 | import com.intuit.developer.sampleapp.webhooks.service.qbo.DataServiceFactory; 20 | import com.intuit.ipp.core.Context; 21 | import com.intuit.ipp.security.OAuthAuthorizer; 22 | import com.intuit.ipp.services.DataService; 23 | import com.intuit.ipp.util.DateUtils; 24 | 25 | /** 26 | * @author dderose 27 | * 28 | */ 29 | @RunWith(SpringJUnit4ClassRunner.class) 30 | @WebAppConfiguration 31 | @ContextConfiguration(loader=AnnotationConfigWebContextLoader.class) 32 | public class CDCServiceTest { 33 | 34 | private CDCService cdcService; 35 | 36 | @Autowired 37 | protected WebApplicationContext wac; 38 | 39 | DataServiceFactory dataServiceFactory; 40 | DataService dataService; 41 | OAuthAuthorizer oAuthAuthorizer; 42 | Context context; 43 | AppConfig appConfig; 44 | 45 | @Before 46 | public void setUp() throws Exception { 47 | MockitoAnnotations.initMocks(this); 48 | cdcService = Mockito.mock(CDCService.class); 49 | dataServiceFactory = Mockito.mock(DataServiceFactory.class); 50 | ReflectionTestUtils.setField(cdcService, "dataServiceFactory", dataServiceFactory); 51 | 52 | dataService = Mockito.mock(DataService.class); 53 | oAuthAuthorizer = Mockito.mock(OAuthAuthorizer.class); 54 | context = Mockito.mock(Context.class); 55 | appConfig = Mockito.mock(AppConfig.class); 56 | 57 | final String consumerKey = "consumerKey"; 58 | final String consumerSecret = "consumerSecret"; 59 | final String appToken = "appToken"; 60 | final String qboUrl = "qbourl"; 61 | 62 | Mockito.when(appConfig.getConsumerKey()).thenReturn(consumerKey); 63 | Mockito.when(appConfig.getConsumerSecret()).thenReturn(consumerSecret); 64 | Mockito.when(appConfig.getAppToken()).thenReturn(appToken); 65 | Mockito.when(appConfig.getQboUrl()).thenReturn(qboUrl); 66 | ReflectionTestUtils.setField(dataServiceFactory, "appConfig", appConfig); 67 | 68 | CompanyConfig companyConfig = new CompanyConfig(); 69 | companyConfig.setLastCdcTimestamp(DateUtils.getStringFromDateTime(DateUtils.getDateWithPrevDays(2))); 70 | Mockito.when(companyConfig.getWebhooksSubscribedEntites()).thenReturn("Customer"); 71 | 72 | Mockito.when(dataServiceFactory.getDataService(companyConfig)).thenReturn(dataService); 73 | 74 | } 75 | 76 | @Test 77 | public void callDataService() throws Exception { 78 | 79 | CompanyConfig companyConfig = new CompanyConfig(); 80 | companyConfig.setLastCdcTimestamp(DateUtils.getStringFromDateTime(DateUtils.getDateWithPrevDays(2))); 81 | companyConfig.setWebhooksSubscribedEntites("Customer"); 82 | 83 | Mockito.doNothing().doThrow(new RuntimeException()).when(cdcService).callDataService(companyConfig); 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/intuit/developer/sampleapp/webhooks/service/security/SecurityService.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.service.security; 2 | 3 | import java.io.UnsupportedEncodingException; 4 | 5 | import javax.annotation.PostConstruct; 6 | import javax.crypto.Cipher; 7 | import javax.crypto.spec.SecretKeySpec; 8 | import javax.xml.bind.DatatypeConverter; 9 | 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.context.annotation.Configuration; 12 | import org.springframework.context.annotation.PropertySource; 13 | import org.springframework.core.env.Environment; 14 | import org.springframework.stereotype.Service; 15 | 16 | import com.intuit.developer.sampleapp.webhooks.service.qbo.WebhooksServiceFactory; 17 | import com.intuit.ipp.services.WebhooksService; 18 | import com.intuit.ipp.util.Config; 19 | import com.intuit.ipp.util.Logger; 20 | 21 | /** 22 | * Utility class for encrypting/decrypting data as well as authenticat 23 | * 24 | * @author dderose 25 | * 26 | */ 27 | @Service 28 | @Configuration 29 | @PropertySource(value="classpath:/application.properties", ignoreResourceNotFound=true) 30 | public class SecurityService { 31 | 32 | private static final org.slf4j.Logger LOG = Logger.getLogger(); 33 | 34 | private static final String VERIFIER_KEY = "webhooks.verifier.token"; 35 | private static final String ENCRYPTION_KEY = "encryption.key"; 36 | 37 | @Autowired 38 | Environment env; 39 | 40 | @Autowired 41 | WebhooksServiceFactory webhooksServiceFactory; 42 | 43 | private SecretKeySpec secretKey; 44 | 45 | @PostConstruct 46 | public void init() { 47 | try { 48 | secretKey = new SecretKeySpec(getEncryptionKey().getBytes("UTF-8"), "AES"); 49 | } catch (UnsupportedEncodingException ex) { 50 | LOG.error("Error during initializing secretkeyspec ", ex.getCause()); 51 | } 52 | } 53 | 54 | /** 55 | * Validates the payload with the intuit-signature hash 56 | * 57 | * @param signature 58 | * @param payload 59 | * @return 60 | */ 61 | public boolean isRequestValid(String signature, String payload) { 62 | 63 | // set custom config 64 | Config.setProperty(Config.WEBHOOKS_VERIFIER_TOKEN, getVerifierKey()); 65 | 66 | // create webhooks service 67 | WebhooksService service = webhooksServiceFactory.getWebhooksService(); 68 | return service.verifyPayload(signature, payload); 69 | } 70 | 71 | /** 72 | * Verified key to validate webhooks payload 73 | * @return 74 | */ 75 | public String getVerifierKey() { 76 | return env.getProperty(VERIFIER_KEY); 77 | } 78 | 79 | /** 80 | * Encryption key 81 | * 82 | * @return 83 | */ 84 | public String getEncryptionKey() { 85 | return env.getProperty(ENCRYPTION_KEY); 86 | } 87 | 88 | /** 89 | * @param plainText 90 | * @return 91 | * @throws Exception 92 | */ 93 | public String encrypt(String plainText) throws Exception { 94 | Cipher aesCipher = Cipher.getInstance("AES"); 95 | aesCipher.init(Cipher.ENCRYPT_MODE, secretKey); 96 | byte[] byteCipherText = aesCipher.doFinal(plainText.getBytes()); 97 | return bytesToHex(byteCipherText); 98 | } 99 | 100 | /** 101 | * @param byteCipherText 102 | * @return 103 | * @throws Exception 104 | */ 105 | public String decrypt(String byteCipherText) throws Exception { 106 | Cipher aesCipher = Cipher.getInstance("AES"); 107 | aesCipher.init(Cipher.DECRYPT_MODE, secretKey); 108 | byte[] bytePlainText = aesCipher.doFinal(hexToBytes(byteCipherText)); 109 | return new String(bytePlainText); 110 | 111 | } 112 | 113 | private String bytesToHex(byte[] hash) { 114 | return DatatypeConverter.printHexBinary(hash); 115 | } 116 | 117 | private byte[] hexToBytes(String hash) { 118 | return DatatypeConverter.parseHexBinary(hash); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/test/java/com/intuit/developer/sampleapp/webhooks/controller/WebhooksControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.intuit.developer.sampleapp.webhooks.controller; 2 | 3 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 4 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; 5 | import static org.springframework.test.web.servlet.setup.MockMvcBuilders.webAppContextSetup; 6 | 7 | import java.io.IOException; 8 | import java.nio.charset.Charset; 9 | 10 | import org.junit.Before; 11 | import org.junit.Test; 12 | import org.junit.runner.RunWith; 13 | import org.mockito.Mockito; 14 | import org.mockito.MockitoAnnotations; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | import org.springframework.http.MediaType; 17 | import org.springframework.test.context.ContextConfiguration; 18 | import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 19 | import org.springframework.test.context.web.AnnotationConfigWebContextLoader; 20 | import org.springframework.test.context.web.WebAppConfiguration; 21 | import org.springframework.test.util.ReflectionTestUtils; 22 | import org.springframework.test.web.servlet.MockMvc; 23 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 24 | import org.springframework.web.context.WebApplicationContext; 25 | 26 | import com.fasterxml.jackson.annotation.JsonInclude; 27 | import com.fasterxml.jackson.databind.ObjectMapper; 28 | import com.intuit.developer.sampleapp.webhooks.controllers.WebhooksController; 29 | import com.intuit.developer.sampleapp.webhooks.service.queue.QueueService; 30 | import com.intuit.developer.sampleapp.webhooks.service.security.SecurityService; 31 | 32 | /** 33 | * @author dderose 34 | * 35 | */ 36 | @RunWith(SpringJUnit4ClassRunner.class) 37 | @WebAppConfiguration 38 | @ContextConfiguration(loader=AnnotationConfigWebContextLoader.class) 39 | public class WebhooksControllerTest { 40 | 41 | private MockMvc mockMvc; 42 | private WebhooksController webhooksController; 43 | private SecurityService securityServiceMock; 44 | private QueueService queueServiceMock; 45 | 46 | public static final MediaType APPLICATION_JSON_UTF8 = new MediaType(MediaType.APPLICATION_JSON.getType(), MediaType.APPLICATION_JSON.getSubtype(), Charset.forName("utf8")); 47 | 48 | 49 | 50 | @Autowired 51 | protected WebApplicationContext wac; 52 | 53 | @Before 54 | public void setUp() throws Exception { 55 | mockMvc = webAppContextSetup(wac).alwaysExpect(status().isOk()).build(); 56 | MockitoAnnotations.initMocks(this); 57 | 58 | webhooksController = new WebhooksController(); 59 | securityServiceMock = Mockito.mock(SecurityService.class); 60 | ReflectionTestUtils.setField(webhooksController, "securityService", securityServiceMock); 61 | 62 | queueServiceMock = Mockito.mock(QueueService.class); 63 | ReflectionTestUtils.setField(webhooksController, "queueService", queueServiceMock); 64 | 65 | Mockito.when(securityServiceMock.isRequestValid(Mockito.anyString(), Mockito.anyString())).thenReturn(true); 66 | 67 | Mockito.doNothing().doThrow(new RuntimeException()).when(queueServiceMock).add(Mockito.anyString()); 68 | 69 | mockMvc = MockMvcBuilders.standaloneSetup(webhooksController).build(); 70 | 71 | } 72 | 73 | @Test 74 | public void webhooks() throws Exception { 75 | 76 | String payload = "{}"; 77 | 78 | mockMvc.perform(post("/webhooks") 79 | .contentType(APPLICATION_JSON_UTF8) 80 | .content(convertObjectToJsonBytes(payload)) 81 | .header("intuit-signature", "1234")) 82 | .andExpect(status().isOk()); 83 | 84 | } 85 | 86 | @Test 87 | public void webhooksWithWrongSignature() throws Exception { 88 | 89 | String payload = "{}"; 90 | 91 | mockMvc.perform(post("/webhooks") 92 | .contentType(APPLICATION_JSON_UTF8) 93 | .content(convertObjectToJsonBytes(payload)) 94 | .header("intuit-signature", "")) 95 | .andExpect(status().isForbidden()); 96 | 97 | } 98 | 99 | public byte[] convertObjectToJsonBytes(Object object) throws IOException { 100 | ObjectMapper mapper = new ObjectMapper(); 101 | mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); 102 | return mapper.writeValueAsBytes(object); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Rate your Sample](views/Ratesample.png)][ss1][![Yes](views/Thumbup.png)][ss2][![No](views/Thumbdown.png)][ss3] 2 | 3 | # SampleApp-Webhooks-Java 4 | SampleApp-Webhooks-Java 5 | 6 |

Welcome to the Intuit Developer's Webhooks Java Sample App.

7 |

This sample app is meant to provide working examples of how to integrate your app with the Intuit Small Business ecosystem. Specifically, this sample application demonstrates the following:

8 | 9 |
    10 |
  • Implementing webhooks endpoint to receive event notifications.
  • 11 |
  • Best practices to be followed while processing the event notifications.
  • 12 |
  • Sample code using QuickBooks Online SDK to call CDC API to sync data between the app and the QuickBooks Online company.
  • 13 |
14 | 15 |

Please note that while these examples work, features not called out above are not intended to be taken and used in production business applications. In other words, this is not a seed project to be taken cart blanche and deployed to your production environment.

16 | 17 |

For example, certain concerns are not addressed at all in our samples (e.g. security, privacy, scalability). In our sample apps, we strive to strike a balance between clarity, maintainability, and performance where we can. However, clarity is ultimately the most important quality in a sample app.

18 | 19 |

Therefore there are certain instances where we might forgo a more complicated implementation (e.g. caching a frequently used value, robust error handling, more generic domain model structure) in favor of code that is easier to read. In that light, we welcome any feedback that makes our samples apps easier to learn from.

20 | 21 | ## Table of Contents 22 | 23 | * [Requirements](#requirements) 24 | * [First Use Instructions](#first-use-instructions) 25 | * [Running the code](#running-the-code) 26 | * [Configuring the endpoint](#configuring-the-endpoint) 27 | * [Project Structure](#project-structure) 28 | * [Reset the App](#reset-the-app) 29 | 30 | 31 | ## Requirements 32 | 33 | In order to successfully run this sample app you need a few things: 34 | 35 | 1. Java 1.8 36 | 2. A [developer.intuit.com](http://developer.intuit.com) account 37 | 3. An app on [developer.intuit.com](http://developer.intuit.com) and the associated app token, consumer key, and consumer secret. 38 | 4. QuickBooks Java SDK (already included in the [`lib`](lib) folder) 39 | 5. Two sandbox companies, connect both companies with your app and generate the oauth tokens. 40 | 41 | ## First Use Instructions 42 | 43 | 1. Clone the GitHub repo to your computer 44 | 2. In [`config.properties`](src/main/resources/config.properties), set oauth.type as 1 or 2 depending on type of app you have. For OAuth2 apps set value as 2. 45 | 3. For OAuth2 apps, fill in the [`config.properties`](src/main/resources/config.properties) file values (companyid, oauth2.accessToken). 46 | 4. For OAuth1 apps, fill in the [`config.properties`](src/main/resources/config.properties) file values (companyid, app token, consumer key, consumer secret, access token key, access token secret). 47 | 5. Also add webhooks subscribed entities and webhooks verifier token that was generated when you subscribed for webhoooks event. 48 | 49 | ## Running the code 50 | 51 | Once the sample app code is on your computer, you can do the following steps to run the app: 52 | 53 | 1. cd to the project directory 54 | 2. Run the command:`./gradlew bootRun` (Mac OS) or `gradlew.bat bootRun` (Windows) 55 | 3. Wait until the terminal output displays the "Started Application in xxx seconds" message. 56 | 4. Open your browser and go to http://localhost:8080/companyConfigs - This will list the companies in the repository for which you have subscribed event notification. 57 | 5. The webhooks endpoint in the sample app is http://localhost:8080/webhooks 58 | 6. Once an event notification is received and processed, you can perform step 4 to see that the last updated timestamp has been updated for the realmId for which notification was received. 59 | 7. To run the code on a different port, uncomment and update server.port property in application.properties 60 | 61 | ## Configuring the endpoint 62 | 63 | Webhooks requires your enpoint to be exposed over the internet. The easiest way to do that while you are still developing your code locally is to use [ngrok](https://ngrok.com/). Here are the steps to configure ngrok 64 | 65 | 1. Download and install ngrok 66 | 2. Expose your localhost by running "./ngrok http 8080" on the command line. 67 | 3. You will then get a forwarding url that looks something like this: 68 | Forwarding https://cb063e9f.ngrok.io -> localhost:8080 69 | (Remember to use only https url and not the http url for webhooks) 70 | This will expose localhost:8080 to the Internet. Your endpoint url will now be https://cb063e9f.ngrok.io/webhooks 71 | Copy this url and use it for setting up webhooks on developer.intuit.com for your app. 72 | 73 | ## Project Structure 74 | * **Standard Java coding structure is used for the sample app** 75 | 76 | * Java code is located in the [`src.main.java`](src/main/java) directory 77 | * Controller classes are in under the controller folder: 78 | - [`WebhooksController.java`](src/main/java/com/intuit/developer/sampleapp/webhooks/controllers/WebhooksController.java) 79 | - [`CompanyController.java`](src/main/java/com/intuit/developer/sampleapp/webhooks/controllers/CompanyController.java) 80 | * Queue implementation and processing classes are in under the service/queue folder: 81 | - [`QueueService.java`](src/main/java/com/intuit/developer/sampleapp/webhooks/service/queue/QueueService.java) 82 | - [`QueueProcessor.java`](src/main/java/com/intuit/developer/sampleapp/webhooks/service/queue/QueueProcessor.java) 83 | * Encryption and payload validation implementation class is in the service/security folder: 84 | - [`SecurityService.java`](src/main/java/com/intuit/developer/sampleapp/webhooks/service/security/SecurityService.java) 85 | * QBO API Service calls are implemented in the service/qbo folder: 86 | - [`CDCService.java`](src/main/java/com/intuit/developer/sampleapp/webhooks/service/qbo/CDCService.java) 87 | - [`QueryService.java`](src/main/java/com/intuit/developer/sampleapp/webhooks/service/qbo/QueryService.java) 88 | 89 | * Property files are located in the [`src.main.resources`](src/main/resources) directory 90 | * JUnit test files are located in the [`src.test.java`](src/test/java) directory 91 | 92 | ## Reset the App 93 | 94 | This app uses an in-memory temporary H2 database. The tables are loaded during startup with realmId and oauth tokens. The table is read and updated when webhooks notification is processed. Stopping the server 95 | will delete the records. 96 | The oauth tokens are encrypted and stored in the database. There is a sample encryption implementation provided using fake keys. For production use real keys, this can be updated in application.properties 97 | 98 | [ss1]: # 99 | [ss2]: https://customersurveys.intuit.com/jfe/form/SV_9LWgJBcyy3NAwHc?check=Yes&checkpoint=SampleApp-Webhooks-Java&pageUrl=github 100 | [ss3]: https://customersurveys.intuit.com/jfe/form/SV_9LWgJBcyy3NAwHc?check=No&checkpoint=SampleApp-Webhooks-Java&pageUrl=github 101 | --------------------------------------------------------------------------------