diff --git a/.gitignore b/.gitignore index a1c2a23..c2065bc 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,37 @@ -# Compiled class file -*.class +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ -# Log file -*.log +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ -# BlueJ files -*.ctxt +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ -# Mobile Tools for Java (J2ME) -.mtj.tmp/ +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* +### VS Code ### +.vscode/ diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..ab05543 --- /dev/null +++ b/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'org.springframework.boot' version '2.4.0' + id 'io.spring.dependency-management' version '1.0.10.RELEASE' + id 'java' + id 'eclipse' + id 'idea' +} + +group = 'com.team23' +version = '0.0.1-SNAPSHOT' +sourceCompatibility = 1.8 +targetCompatibility = 1.8 + +repositories { + mavenCentral() + mavenLocal() +} + +dependencies { + compile("com.intuit.quickbooks-online:ipp-v3-java-data:4.0.1") + compile (group: 'com.intuit.quickbooks-online', name: 'ipp-v3-java-devkit', version: '4.0.1', classifier: 'jar-with-dependencies') + compile (group: 'com.intuit.quickbooks-online', name: 'oauth2-platform-api', version: '4.0.1', classifier: 'jar-with-dependencies') + compile("org.springframework.boot:spring-boot-starter-web") + compile("org.springframework.boot:spring-boot-starter-thymeleaf") + compile("org.springframework.data:spring-data-rest-webmvc") + compile("org.json:json") + compile("log4j:log4j:1.2.17") + compile (group: 'ant', name: 'ant', version: '1.7.0') + testCompile('org.springframework.boot:spring-boot-starter-test') +} + +test { + useJUnitPlatform() +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..7063285 --- /dev/null +++ b/gradle.properties @@ -0,0 +1 @@ +org.gradle.java.home=C:\\Program Files\\AdoptOpenJDK\\jdk-8.0.275.1-hotspot \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..12d38de --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# 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 +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# 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. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..107acd3 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..dfcdbef --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'stim' diff --git a/src/main/java/com/team23/stim/Application.java b/src/main/java/com/team23/stim/Application.java new file mode 100644 index 0000000..eaab144 --- /dev/null +++ b/src/main/java/com/team23/stim/Application.java @@ -0,0 +1,22 @@ +package com.team23.stim; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration; + +/** + * @author dderose + * + */ +@SpringBootApplication +public class Application { + + /** + * @param args + */ + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } + + +} diff --git a/src/main/java/com/team23/stim/client/OAuth2PlatformClientFactory.java b/src/main/java/com/team23/stim/client/OAuth2PlatformClientFactory.java new file mode 100644 index 0000000..a1a9658 --- /dev/null +++ b/src/main/java/com/team23/stim/client/OAuth2PlatformClientFactory.java @@ -0,0 +1,51 @@ +package com.team23.stim.client; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.PropertySource; +import org.springframework.stereotype.Service; + +import com.intuit.oauth2.client.OAuth2PlatformClient; +import com.intuit.oauth2.config.Environment; +import com.intuit.oauth2.config.OAuth2Config; + +/** + * + * @author dderose + * + */ +@Service +@PropertySource(value="classpath:/application.properties", ignoreResourceNotFound=true) +public class OAuth2PlatformClientFactory { + + @Autowired + org.springframework.core.env.Environment env; + + OAuth2PlatformClient client; + OAuth2Config oauth2Config; + + @PostConstruct + public void init() { + //initialize the config + oauth2Config = new OAuth2Config.OAuth2ConfigBuilder(env.getProperty("OAuth2AppClientId"), env.getProperty("OAuth2AppClientSecret")) //set client id, secret + .callDiscoveryAPI(Environment.SANDBOX) // call discovery API to populate urls + .buildConfig(); + //build the client + client = new OAuth2PlatformClient(oauth2Config); + } + + + public OAuth2PlatformClient getOAuth2PlatformClient() { + return client; + } + + public OAuth2Config getOAuth2Config() { + return oauth2Config; + } + + public String getPropertyValue(String proppertyName) { + return env.getProperty(proppertyName); + } + +} diff --git a/src/main/java/com/team23/stim/controller/CallbackController.java b/src/main/java/com/team23/stim/controller/CallbackController.java new file mode 100644 index 0000000..621666e --- /dev/null +++ b/src/main/java/com/team23/stim/controller/CallbackController.java @@ -0,0 +1,70 @@ +package com.team23.stim.controller; + +import javax.servlet.http.HttpSession; + +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import com.team23.stim.client.OAuth2PlatformClientFactory; +import com.intuit.oauth2.client.OAuth2PlatformClient; +import com.intuit.oauth2.data.BearerTokenResponse; +import com.intuit.oauth2.exception.OAuthException; + +/** + * @author dderose + * + */ +@Controller +public class CallbackController { + + @Autowired + OAuth2PlatformClientFactory factory; + + private static final Logger logger = Logger.getLogger(CallbackController.class); + + /** + * This is the redirect handler you configure in your app on developer.intuit.com + * The Authorization code has a short lifetime. + * Hence Unless a user action is quick and mandatory, proceed to exchange the Authorization Code for + * BearerToken + * + * @param auth_code + * @param state + * @param realmId + * @param session + * @return + */ + @RequestMapping("/oauth2redirect") + public String callBackFromOAuth(@RequestParam("code") String authCode, @RequestParam("state") String state, @RequestParam(value = "realmId", required = false) String realmId, HttpSession session) { + logger.info("inside oauth2redirect of sample" ); + try { + String csrfToken = (String) session.getAttribute("csrfToken"); + if (csrfToken.equals(state)) { + session.setAttribute("realmId", realmId); + session.setAttribute("auth_code", authCode); + + OAuth2PlatformClient client = factory.getOAuth2PlatformClient(); + String redirectUri = factory.getPropertyValue("OAuth2AppRedirectUri"); + logger.info("inside oauth2redirect of sample -- redirectUri " + redirectUri ); + + BearerTokenResponse bearerTokenResponse = client.retrieveBearerTokens(authCode, redirectUri); + + session.setAttribute("access_token", bearerTokenResponse.getAccessToken()); + session.setAttribute("refresh_token", bearerTokenResponse.getRefreshToken()); + + // Update your Data store here with user's AccessToken and RefreshToken along with the realmId + + return "connected"; + } + logger.info("csrf token mismatch " ); + } catch (OAuthException e) { + logger.error("Exception in callback handler ", e); + } + return null; + } + + +} diff --git a/src/main/java/com/team23/stim/controller/HomeController.java b/src/main/java/com/team23/stim/controller/HomeController.java new file mode 100644 index 0000000..8f2911c --- /dev/null +++ b/src/main/java/com/team23/stim/controller/HomeController.java @@ -0,0 +1,69 @@ +package com.team23.stim.controller; + +import java.util.ArrayList; +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.servlet.View; +import org.springframework.web.servlet.view.RedirectView; + +import com.team23.stim.client.OAuth2PlatformClientFactory; +import com.intuit.oauth2.config.OAuth2Config; +import com.intuit.oauth2.config.Scope; +import com.intuit.oauth2.exception.InvalidRequestException; + +/** + * @author dderose + * + */ +@Controller +public class HomeController { + + private static final Logger logger = Logger.getLogger(HomeController.class); + + @Autowired + OAuth2PlatformClientFactory factory; + + @RequestMapping("/") + public String home() { + return "home"; + } + + @RequestMapping("/connected") + public String connected() { + return "connected"; + } + + @RequestMapping("/testpage") + public String testpage(){ + return "testpage"; + } + /** + * Controller mapping for connectToQuickbooks button + * @return + */ + @RequestMapping("/connectToQuickbooks") + public View connectToQuickbooks(HttpSession session) { + logger.info("inside connectToQuickbooks "); + OAuth2Config oauth2Config = factory.getOAuth2Config(); + + String redirectUri = factory.getPropertyValue("OAuth2AppRedirectUri"); + + String csrf = oauth2Config.generateCSRFToken(); + session.setAttribute("csrfToken", csrf); + try { + List scopes = new ArrayList(); + scopes.add(Scope.Accounting); + return new RedirectView(oauth2Config.prepareUrl(scopes, redirectUri, csrf), true, true, false); + } catch (InvalidRequestException e) { + logger.error("Exception calling connectToQuickbooks ", e); + } + return null; + } + +} diff --git a/src/main/java/com/team23/stim/controller/InventoryController.java b/src/main/java/com/team23/stim/controller/InventoryController.java new file mode 100644 index 0000000..44c6fc5 --- /dev/null +++ b/src/main/java/com/team23/stim/controller/InventoryController.java @@ -0,0 +1,307 @@ +package com.team23.stim.controller; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.apache.log4j.Logger; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.team23.stim.client.OAuth2PlatformClientFactory; +import com.team23.stim.helper.QBOServiceHelper; +import com.intuit.ipp.core.IEntity; +import com.intuit.ipp.data.Account; +import com.intuit.ipp.data.AccountSubTypeEnum; +import com.intuit.ipp.data.AccountTypeEnum; +import com.intuit.ipp.data.Customer; +import com.intuit.ipp.data.EmailAddress; +import com.intuit.ipp.data.Error; +import com.intuit.ipp.data.IntuitEntity; +import com.intuit.ipp.data.Invoice; +import com.intuit.ipp.data.Item; +import com.intuit.ipp.data.ItemTypeEnum; +import com.intuit.ipp.data.Line; +import com.intuit.ipp.data.LineDetailTypeEnum; +import com.intuit.ipp.data.ReferenceType; +import com.intuit.ipp.data.SalesItemLineDetail; +import com.intuit.ipp.exception.FMSException; +import com.intuit.ipp.exception.InvalidTokenException; +import com.intuit.ipp.services.DataService; +import com.intuit.ipp.services.QueryResult; + +/** + * @author bcole + * + */ +@Controller +public class InventoryController { + + @Autowired + OAuth2PlatformClientFactory factory; + + @Autowired + public QBOServiceHelper helper; + + private static final Logger logger = Logger.getLogger(InventoryController.class); + + private static final String ACCOUNT_QUERY = "select * from Account where AccountType='Sales of Product Income' maxresults 1"; + + + /** + * Sample QBO API call using OAuth2 tokens + * + * @param session + * @return + */ + @ResponseBody + @RequestMapping("/inventory") + public String callInventoryConcept(HttpSession session) { + + String realmId = (String)session.getAttribute("realmId"); + if (StringUtils.isEmpty(realmId)) { + return new JSONObject().put("response", "No realm ID. QBO calls only work if the accounting scope was passed!").toString(); + } + String accessToken = (String)session.getAttribute("access_token"); + + try { + + // Get DataService + DataService service = helper.getDataService(realmId, accessToken); + + // Add inventory item - with initial Quantity on Hand of 10 + Item item = getItemWithAllFields(service); + Item savedItem = service.add(item); + + // Create invoice (for 1 item) using the item created above + Customer customer = getCustomerWithAllFields(); + Customer savedCustomer = service.add(customer); + Invoice invoice = getInvoiceFields(savedCustomer, savedItem); + service.add(invoice); + + // Query inventory item - there should be 9 items now! + Item itemsRemaining = service.findById(savedItem); + + // Return response back - take a look at "qtyOnHand" in the output (should be 9) + return createResponse(itemsRemaining); + + } catch (InvalidTokenException e) { + return new JSONObject().put("response", "InvalidToken - Refresh token and try again").toString(); + } catch (FMSException e) { + List list = e.getErrorList(); + list.forEach(error -> logger.error("Error while calling the API :: " + error.getMessage())); + return new JSONObject().put("response","Failed").toString(); + } + } + + + /** + * Prepare Item request + * @param service + * @return + * @throws FMSException + */ + private Item getItemWithAllFields(DataService service) throws FMSException { + Item item = new Item(); + item.setType(ItemTypeEnum.INVENTORY); + item.setName("Inventory Item " + RandomStringUtils.randomAlphanumeric(5)); + item.setInvStartDate(new Date()); + + // Start with 10 items + item.setQtyOnHand(BigDecimal.valueOf(10)); + item.setTrackQtyOnHand(true); + + Account incomeBankAccount = getIncomeBankAccount(service); + item.setIncomeAccountRef(createRef(incomeBankAccount)); + + Account expenseBankAccount = getExpenseBankAccount(service); + item.setExpenseAccountRef(createRef(expenseBankAccount)); + + Account assetAccount = getAssetAccount(service); + item.setAssetAccountRef(createRef(assetAccount)); + + return item; + } + + /** + * Prepare Customer request + * @return + */ + private Customer getCustomerWithAllFields() { + Customer customer = new Customer(); + customer.setDisplayName(org.apache.commons.lang.RandomStringUtils.randomAlphanumeric(6)); + customer.setCompanyName("ABC Corporations"); + + EmailAddress emailAddr = new EmailAddress(); + emailAddr.setAddress("testconceptsample@mailinator.com"); + customer.setPrimaryEmailAddr(emailAddr); + + return customer; + } + + /** + * Prepare Invoice Request + * @param customer + * @param item + * @return + */ + private Invoice getInvoiceFields(Customer customer, Item item) { + Invoice invoice = new Invoice(); + invoice.setCustomerRef(createRef(customer)); + + List invLine = new ArrayList(); + Line line = new Line(); + line.setAmount(new BigDecimal("100")); + line.setDetailType(LineDetailTypeEnum.SALES_ITEM_LINE_DETAIL); + + SalesItemLineDetail silDetails = new SalesItemLineDetail(); + silDetails.setQty(BigDecimal.valueOf(1)); + silDetails.setItemRef(createRef(item)); + + line.setSalesItemLineDetail(silDetails); + invLine.add(line); + invoice.setLine(invLine); + + return invoice; + } + + + + /** + * Get Income Account + * @param service + * @return + * @throws FMSException + */ + private Account getIncomeBankAccount(DataService service) throws FMSException { + QueryResult queryResult = service.executeQuery(String.format(ACCOUNT_QUERY, AccountTypeEnum.INCOME.value(), AccountSubTypeEnum.SALES_OF_PRODUCT_INCOME.value())); + List entities = queryResult.getEntities(); + if(!entities.isEmpty()) { + return (Account)entities.get(0); + } + return createIncomeBankAccount(service); + } + + /** + * Create Income Account + * @param service + * @return + * @throws FMSException + */ + private Account createIncomeBankAccount(DataService service) throws FMSException { + Account account = new Account(); + account.setName("Income " + RandomStringUtils.randomAlphabetic(5)); + account.setAccountType(AccountTypeEnum.INCOME); + account.setAccountSubType(AccountSubTypeEnum.SALES_OF_PRODUCT_INCOME.value()); + + return service.add(account); + } + + /** + * Get Expense Account + * @param service + * @return + * @throws FMSException + */ + private Account getExpenseBankAccount(DataService service) throws FMSException { + QueryResult queryResult = service.executeQuery(String.format(ACCOUNT_QUERY, AccountTypeEnum.COST_OF_GOODS_SOLD.value(), AccountSubTypeEnum.SUPPLIES_MATERIALS_COGS.value())); + List entities = queryResult.getEntities(); + if(!entities.isEmpty()) { + return (Account)entities.get(0); + } + return createExpenseBankAccount(service); + } + + /** + * Create Expense Account + * @param service + * @return + * @throws FMSException + */ + private Account createExpenseBankAccount(DataService service) throws FMSException { + Account account = new Account(); + account.setName("Expense " + RandomStringUtils.randomAlphabetic(5)); + account.setAccountType(AccountTypeEnum.COST_OF_GOODS_SOLD); + account.setAccountSubType(AccountSubTypeEnum.SUPPLIES_MATERIALS_COGS.value()); + + return service.add(account); + } + + + /** + * Get Asset Account + * @param service + * @return + * @throws FMSException + */ + private Account getAssetAccount(DataService service) throws FMSException{ + QueryResult queryResult = service.executeQuery(String.format(ACCOUNT_QUERY, AccountTypeEnum.OTHER_CURRENT_ASSET.value(), AccountSubTypeEnum.INVENTORY.value())); + List entities = queryResult.getEntities(); + if(!entities.isEmpty()) { + return (Account)entities.get(0); + } + return createOtherCurrentAssetAccount(service); + } + + /** + * Create Asset Account + * @param service + * @return + * @throws FMSException + */ + private Account createOtherCurrentAssetAccount(DataService service) throws FMSException { + Account account = new Account(); + account.setName("Other Current Asset " + RandomStringUtils.randomAlphanumeric(5)); + account.setAccountType(AccountTypeEnum.OTHER_CURRENT_ASSET); + account.setAccountSubType(AccountSubTypeEnum.INVENTORY.value()); + + return service.add(account); + } + + /** + * Creates reference type for an entity + * + * @param entity - IntuitEntity object inherited by each entity + * @return + */ + private ReferenceType createRef(IntuitEntity entity) { + ReferenceType referenceType = new ReferenceType(); + referenceType.setValue(entity.getId()); + return referenceType; + } + + /** + * Map object to json string + * @param entity + * @return + */ + private String createResponse(Object entity) { + ObjectMapper mapper = new ObjectMapper(); + String jsonInString; + try { + jsonInString = mapper.writeValueAsString(entity); + } catch (JsonProcessingException e) { + return createErrorResponse(e); + } catch (Exception e) { + return createErrorResponse(e); + } + return jsonInString; + } + + private String createErrorResponse(Exception e) { + logger.error("Exception while calling QBO ", e); + return new JSONObject().put("response","Failed").toString(); + } + + +} \ No newline at end of file diff --git a/src/main/java/com/team23/stim/controller/QBOController.java b/src/main/java/com/team23/stim/controller/QBOController.java new file mode 100644 index 0000000..5ee1e8d --- /dev/null +++ b/src/main/java/com/team23/stim/controller/QBOController.java @@ -0,0 +1,138 @@ +package com.team23.stim.controller; + +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.json.JSONObject; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseBody; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.team23.stim.client.OAuth2PlatformClientFactory; +import com.team23.stim.helper.QBOServiceHelper; +import com.intuit.ipp.data.CompanyInfo; +import com.intuit.ipp.data.Error; +import com.intuit.ipp.exception.FMSException; +import com.intuit.ipp.exception.InvalidTokenException; +import com.intuit.ipp.services.DataService; +import com.intuit.ipp.services.QueryResult; +import com.intuit.oauth2.client.OAuth2PlatformClient; +import com.intuit.oauth2.data.BearerTokenResponse; +import com.intuit.oauth2.exception.OAuthException; + +/** + * @author dderose + * + */ +@Controller +public class QBOController { + + @Autowired + OAuth2PlatformClientFactory factory; + + @Autowired + public QBOServiceHelper helper; + + + private static final Logger logger = Logger.getLogger(QBOController.class); + private static final String failureMsg="Failed"; + + + /** + * Sample QBO API call using OAuth2 tokens + * + * @param session + * @return + */ + @ResponseBody + @RequestMapping("/getCompanyInfo") + public String callQBOCompanyInfo(HttpSession session) { + + String realmId = (String)session.getAttribute("realmId"); + if (StringUtils.isEmpty(realmId)) { + return new JSONObject().put("response","No realm ID. QBO calls only work if the accounting scope was passed!").toString(); + } + String accessToken = (String)session.getAttribute("access_token"); + + try { + + + //get DataService + DataService service = helper.getDataService(realmId, accessToken); + + // get all companyinfo + String sql = "select * from companyinfo"; + QueryResult queryResult = service.executeQuery(sql); + return processResponse(failureMsg, queryResult); + + } + /* + * Handle 401 status code - + * If a 401 response is received, refresh tokens should be used to get a new access token, + * and the API call should be tried again. + */ + catch (InvalidTokenException e) { + logger.error("Error while calling executeQuery :: " + e.getMessage()); + + //refresh tokens + logger.info("received 401 during companyinfo call, refreshing tokens now"); + OAuth2PlatformClient client = factory.getOAuth2PlatformClient(); + String refreshToken = (String)session.getAttribute("refresh_token"); + + try { + BearerTokenResponse bearerTokenResponse = client.refreshToken(refreshToken); + session.setAttribute("access_token", bearerTokenResponse.getAccessToken()); + session.setAttribute("refresh_token", bearerTokenResponse.getRefreshToken()); + + //call company info again using new tokens + logger.info("calling companyinfo using new tokens"); + DataService service = helper.getDataService(realmId, accessToken); + + // get all companyinfo + String sql = "select * from companyinfo"; + QueryResult queryResult = service.executeQuery(sql); + return processResponse(failureMsg, queryResult); + + } catch (OAuthException e1) { + logger.error("Error while calling bearer token :: " + e.getMessage()); + return new JSONObject().put("response",failureMsg).toString(); + } catch (FMSException e1) { + logger.error("Error while calling company currency :: " + e.getMessage()); + return new JSONObject().put("response",failureMsg).toString(); + } + + } catch (FMSException e) { + List list = e.getErrorList(); + list.forEach(error -> logger.error("Error while calling executeQuery :: " + error.getMessage())); + return new JSONObject().put("response",failureMsg).toString(); + } + + } + + private String processResponse(String failureMsg, QueryResult queryResult) { + if (!queryResult.getEntities().isEmpty() && queryResult.getEntities().size() > 0) { + CompanyInfo companyInfo = (CompanyInfo) queryResult.getEntities().get(0); + logger.info("Companyinfo -> CompanyName: " + companyInfo.getCompanyName()); + ObjectMapper mapper = new ObjectMapper(); + try { + String jsonInString = mapper.writeValueAsString(companyInfo); + return jsonInString; + } catch (JsonProcessingException e) { + logger.error("Exception while getting company info ", e); + return new JSONObject().put("response",failureMsg).toString(); + } + + } + return failureMsg; + } + + + + +} diff --git a/src/main/java/com/team23/stim/helper/QBOServiceHelper.java b/src/main/java/com/team23/stim/helper/QBOServiceHelper.java new file mode 100644 index 0000000..41c37c0 --- /dev/null +++ b/src/main/java/com/team23/stim/helper/QBOServiceHelper.java @@ -0,0 +1,114 @@ +package com.team23.stim.helper; + +import java.util.List; + +import javax.servlet.http.HttpSession; + +import org.apache.commons.lang.StringUtils; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.team23.stim.client.OAuth2PlatformClientFactory; +import com.intuit.ipp.core.Context; +import com.intuit.ipp.core.IEntity; +import com.intuit.ipp.core.ServiceType; +import com.intuit.ipp.data.Error; +import com.intuit.ipp.exception.FMSException; +import com.intuit.ipp.exception.InvalidTokenException; +import com.intuit.ipp.security.OAuth2Authorizer; +import com.intuit.ipp.services.DataService; +import com.intuit.ipp.services.QueryResult; +import com.intuit.ipp.util.Config; +import com.intuit.oauth2.client.OAuth2PlatformClient; +import com.intuit.oauth2.data.BearerTokenResponse; +import com.intuit.oauth2.exception.OAuthException; + +@Service +public class QBOServiceHelper { + + @Autowired + OAuth2PlatformClientFactory factory; + + private static final Logger logger = Logger.getLogger(QBOServiceHelper.class); + + public DataService getDataService(String realmId, String accessToken) throws FMSException { + + String url = factory.getPropertyValue("IntuitAccountingAPIHost") + "/v3/company"; + + Config.setProperty(Config.BASE_URL_QBO, url); + //create oauth object + OAuth2Authorizer oauth = new OAuth2Authorizer(accessToken); + //create context + Context context = new Context(oauth, ServiceType.QBO, realmId); + + // create dataservice + return new DataService(context); + } + + + + /** + * Queries data from QuickBooks + * + * @param session + * @param sql + * @return + */ + public List queryData(HttpSession session, String sql) { + + String realmId = (String)session.getAttribute("realmId"); + if (StringUtils.isEmpty(realmId)) { + logger.error("Relam id is null "); + } + String accessToken = (String)session.getAttribute("access_token"); + + try { + + //get DataService + DataService service = getDataService(realmId, accessToken); + + // get data + QueryResult queryResult = service.executeQuery(sql); + return queryResult.getEntities(); + } + /* + * Handle 401 status code - + * If a 401 response is received, refresh tokens should be used to get a new access token, + * and the API call should be tried again. + */ + catch (InvalidTokenException e) { + logger.error("Error while calling executeQuery :: " + e.getMessage()); + + //refresh tokens + logger.info("received 401 during companyinfo call, refreshing tokens now"); + OAuth2PlatformClient client = factory.getOAuth2PlatformClient(); + String refreshToken = (String)session.getAttribute("refresh_token"); + + try { + BearerTokenResponse bearerTokenResponse = client.refreshToken(refreshToken); + session.setAttribute("access_token", bearerTokenResponse.getAccessToken()); + session.setAttribute("refresh_token", bearerTokenResponse.getRefreshToken()); + + //call company info again using new tokens + logger.info("calling companyinfo using new tokens"); + DataService service = getDataService(realmId, accessToken); + + // get data + QueryResult queryResult = service.executeQuery(sql); + return queryResult.getEntities(); + + } catch (OAuthException e1) { + logger.error("Error while calling bearer token :: " + e.getMessage()); + + } catch (FMSException e1) { + logger.error("Error while calling company currency :: " + e.getMessage()); + } + + } catch (FMSException e) { + List list = e.getErrorList(); + list.forEach(error -> logger.error("Error while calling executeQuery :: " + error.getMessage())); + } + return null; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..307ee50 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,13 @@ +#OAuth2 App Configuration +OAuth2AppClientId=ABEgXRhxPGsV3JK3DxN414plVxJaA8om5xqrGJdw9Ms462jAj9 +OAuth2AppClientSecret=AuE3j7oiRFRCZKESnSrMnUTvNRjYMSvlJwnPMyHJ +OAuth2AppRedirectUri=http://localhost:8080/oauth2redirect + +#QBO API endpoint +IntuitAccountingAPIHost=https://sandbox-quickbooks.api.intuit.com +#IntuitAccountingAPIHost=https://quickbooks.api.intuit.com + +spring.thymeleaf.cache=false +server.port = 8080 + +logging.level.org.apache.http=DEBUG \ No newline at end of file diff --git a/src/main/resources/log4j.properties b/src/main/resources/log4j.properties new file mode 100644 index 0000000..8cbd17d --- /dev/null +++ b/src/main/resources/log4j.properties @@ -0,0 +1,9 @@ +log4j.rootLogger=DEBUG, STDOUT + +#This code sets SDK logger at level ERROR and overrides the rootLogger level. +log4j.logger.com.intuit.platform=DEBUG + +#This appender will log messages to the console. +log4j.appender.STDOUT=org.apache.log4j.ConsoleAppender +log4j.appender.STDOUT.layout=org.apache.log4j.PatternLayout +log4j.appender.STDOUT.layout.ConversionPattern=%-4r [%t] %-5p %c %x - %m%n \ No newline at end of file diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css new file mode 100644 index 0000000..f83471d --- /dev/null +++ b/src/main/resources/static/css/style.css @@ -0,0 +1,15 @@ +body { + font-family: "Helvetica Neue",HelveticaNeue,Helvetica,Arial,sans-serif; +} + + + +.imgLink { + text-decoration: none; + display: inline-block; + margin: 10px; +} + +#result { + white-space: pre-wrap; +} diff --git a/src/main/resources/static/images/C2QB_white_btn_lg_default.png b/src/main/resources/static/images/C2QB_white_btn_lg_default.png new file mode 100644 index 0000000..05d95cc Binary files /dev/null and b/src/main/resources/static/images/C2QB_white_btn_lg_default.png differ diff --git a/src/main/resources/static/images/C2QB_white_btn_lg_hover.png b/src/main/resources/static/images/C2QB_white_btn_lg_hover.png new file mode 100644 index 0000000..197b7b5 Binary files /dev/null and b/src/main/resources/static/images/C2QB_white_btn_lg_hover.png differ diff --git a/src/main/resources/templates/connected.html b/src/main/resources/templates/connected.html new file mode 100644 index 0000000..7eb25fa --- /dev/null +++ b/src/main/resources/templates/connected.html @@ -0,0 +1,47 @@ + + + + STIM + + + + + + STIM Authorization Page +

Connected!

+ +
+ +

+
+
+ + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html new file mode 100644 index 0000000..6cda938 --- /dev/null +++ b/src/main/resources/templates/home.html @@ -0,0 +1,46 @@ + + + STIM + + + +

STIM Auhorization Page

+ A QuickBooks account is required to use this application. +
    +
  • + The Sandbox company will be used. +
  • + +
  • + This is an in-progress prototype. +
  • +
+


+ + + + Connect To QuickBooks
+ + + +


+ + + + + + + diff --git a/src/main/resources/templates/testpage.html b/src/main/resources/templates/testpage.html new file mode 100644 index 0000000..e27b43b --- /dev/null +++ b/src/main/resources/templates/testpage.html @@ -0,0 +1,45 @@ + + + This is a test page! + + + +

We're doing a whole lot of testing.

+ Before using this app, please make sure you do the following: + +


+ + + + Connect To QuickBooks
+ + + +


+ + + + + + + diff --git a/src/test/java/com/team23/stim/StimApplicationTests.java b/src/test/java/com/team23/stim/StimApplicationTests.java new file mode 100644 index 0000000..47141da --- /dev/null +++ b/src/test/java/com/team23/stim/StimApplicationTests.java @@ -0,0 +1,13 @@ +package com.team23.stim; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class StimApplicationTests { + + @Test + void contextLoads() { + } + +}