diff --git a/.gitignore b/.gitignore index af9eba2f..fdb6ddb9 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,11 @@ pids *.seed *.pid.lock +# Java backend files +server/java/build/* +server/java/gradle/* +server/java/out/* + # Directory for instrumented libs generated by jscoverage/JSCover lib-cov @@ -61,4 +66,4 @@ typings/ .env # serverless -serverless/ \ No newline at end of file +serverless/ diff --git a/server/java/README.md b/server/java/README.md new file mode 100644 index 00000000..6205ef4c --- /dev/null +++ b/server/java/README.md @@ -0,0 +1,83 @@ +# Stripe Payments Demo - Java Server + +This demo uses a simple [Spark](http://sparkjava.com) application as the server. + +## Payments Integration + +- [`Application.java`](src/main/java/app/Application.java) contains the routes that interface with Stripe to create charges and receive webhook events. + +## Requirements + +You’ll new the following: + +- [Java 8](https://www.oracle.com/technetwork/java/javase/overview/java8-2100321.html) +- [Gradle](https://gradle.org/) +- Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay). +- Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free!) + +## Getting Started + +Before getting started check that you have java installed + +``` +java -version +``` + +Set the enviroment variables for the web application. You can copy the environment variables from [here](../../.env.example) + +``` +export STRIPE_PUBLISHABLE_KEY= +export STRIPE_SECRET_KEY= +export STRIPE_WEBHOOK_SECRET= +export STRIPE_ACCOUNT_COUNTRY= +export PAYMENT_METHODS= +export NGROK_SUBDOMAIN= +export NGROK_AUTHTOKEN= +``` + +Use `gradle` to install the required dependencies by navigating to ./server/java and running: + +``` +gradle build +``` + +Use `gradle` to build a "fatjar" including all the apps dependencies. + +``` +gradle shadowJar +``` + +This will build a JAR in the `./build/libs` directory. Running this JAR will start the Spark Web Application on port 4567. + +``` +java -jar build/libs/salesPaymentDemo-1.0-SNAPSHOT.jar +``` + +You should now see it running on [`http://localhost:4567/`](http://localhost:4567/) + +### Testing Webhooks + +#### :warning: API Versions + +Java is a strictly typed language. As such when deserializing objects using the Stripe Java SDK one should ensure that the API version of their account is +compatible with the API version the Stripe Java SDK you are using. If you are using a new SDK and have an older API Version on your account you may see errors +when trying to deserialize or use deserialized objects. + +You can see which version of the Java SDK Matches which API Version [here](https://github.com/stripe/stripe-java/blob/master/src/main/java/com/stripe/Stripe.java#L13) + +If you want to test [receiving webhooks](https://stripe.com/docs/webhooks), we recommend using ngrok to expose your local server. + +First [download ngrok](https://ngrok.com) and start your Spark application. + +[Run ngrok](https://ngrok.com/docs). Assuming your Spark application is running on the default port 4567, you can simply run ngrok in your Terminal in the directory where you downloaded ngrok: + +``` +ngrok http 4567 +``` + +ngrok will display a UI in your terminal telling you the new forwarding address for your Spark app. Use this URL as the URL to be called in your developer [webhooks panel.](https://dashboard.stripe.com/account/webhooks) + +Don't forget to append `/webhook` when you set up your Stripe webhook URL in the Dashboard. Example URL to be called: `https://75795038.ngrok.io/webhook`. + +## Credits +- Code: [Mike Shaw](https://www.linkedin.com/in/mandshaw/) diff --git a/server/java/build.gradle b/server/java/build.gradle new file mode 100644 index 00000000..3bea7647 --- /dev/null +++ b/server/java/build.gradle @@ -0,0 +1,29 @@ +plugins { + id 'java' + id 'com.github.johnrengelman.shadow' version '4.0.4' +} + +group 'com.stripe' +version '1.0-SNAPSHOT' + +sourceCompatibility = 1.8 + +repositories { + mavenCentral() +} + +jar { + manifest { + attributes( + 'Main-Class': 'app.Application' + ) + } +} + +dependencies { + compile 'com.sparkjava:spark-core:2.8.0' + compile 'org.slf4j:slf4j-simple:1.7.21' + compile 'com.google.code.gson:gson:2.8.0' + compile 'com.stripe:stripe-java:8.1.0' + testCompile group: 'junit', name: 'junit', version: '4.12' +} \ No newline at end of file diff --git a/server/java/gradlew b/server/java/gradlew new file mode 100755 index 00000000..cccdd3d5 --- /dev/null +++ b/server/java/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## 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="" + +# 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, switch paths to Windows format before running java +if $cygwin ; 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=$((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" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/server/java/gradlew.bat b/server/java/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/server/java/gradlew.bat @@ -0,0 +1,84 @@ +@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 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= + +@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 init + +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 init + +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 + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +: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 %CMD_LINE_ARGS% + +: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/server/java/settings.gradle b/server/java/settings.gradle new file mode 100644 index 00000000..0f866736 --- /dev/null +++ b/server/java/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'salesPaymentDemo' + diff --git a/server/java/src/main/java/app/Application.java b/server/java/src/main/java/app/Application.java new file mode 100644 index 00000000..185a8584 --- /dev/null +++ b/server/java/src/main/java/app/Application.java @@ -0,0 +1,26 @@ +package app; + +import app.config.ConfigController; +import app.fulfillment.FulfillmentController; +import app.product.ProductController; +import app.payment.PaymentController; + +import static spark.Spark.*; + +public class Application { + + public static void main(String[] args) { + port(4567); + staticFiles.externalLocation("../../public"); + staticFiles.expireTime(600L); + + get("/config", ConfigController.getConfig); + get("/products", ProductController.getProducts); + get("/products/:id", ProductController.getProduct); + get("/product/:id/skus", ProductController.getSKUsForProduct); + get("/payment_intents/:id/status", PaymentController.getPaymentIntent); + post("/payment_intents", PaymentController.createPaymentIntent); + post("/payment_intents/:id/shipping_change", PaymentController.updatePaymentIntent); + post("/webhook", FulfillmentController.webhookReceived); + } +} \ No newline at end of file diff --git a/server/java/src/main/java/app/config/Config.java b/server/java/src/main/java/app/config/Config.java new file mode 100644 index 00000000..9fab7b96 --- /dev/null +++ b/server/java/src/main/java/app/config/Config.java @@ -0,0 +1,43 @@ +package app.config; + +import app.config.ShippingOptions.*; +import java.util.*; + + +/** + * Class to store Config for the Demo + * @author Michael Shaw + */ +public class Config { + public String stripePublishableKey; + public String stripeCountry; + public String country; + public String currency; + public List paymentMethods; + public List shippingOptions; + + public Config() { + this.stripePublishableKey = System.getenv("STRIPE_PUBLISHABLE_KEY"); + this.stripeCountry = Optional.ofNullable(System.getenv("STRIPE_ACCOUNT_COUNTRY")).orElse("US"); + this.country = "US"; + this.currency = "eur"; + this.paymentMethods = Arrays.asList(Optional.ofNullable(System.getenv("PAYMENT_METHODS")).orElse("card").split("\\s*,\\s*")); + this.shippingOptions = new ArrayList(); + + ShippingOptions option1 = new ShippingOptions(); + option1.id = "free"; + option1.label = "Free Shipping"; + option1.detail = "Delivery within 5 days"; + option1.amount = 0; + + this.shippingOptions.add(option1); + + ShippingOptions option2 = new ShippingOptions(); + option2.id = "express"; + option2.label = "Express Shipping"; + option2.detail = "Next day delivery"; + option2.amount = 500; + + this.shippingOptions.add(option2); + } +} diff --git a/server/java/src/main/java/app/config/ConfigController.java b/server/java/src/main/java/app/config/ConfigController.java new file mode 100644 index 00000000..263089bc --- /dev/null +++ b/server/java/src/main/java/app/config/ConfigController.java @@ -0,0 +1,18 @@ +package app.config; + +import com.google.gson.*; +import spark.*; + +public class ConfigController { + + public static Route getConfig = (Request request, Response response) -> { + + Config config = new Config(); + + response.status(200); + response.type("application/json"); + + // Return as camel case as JS is expecting that + return new Gson().toJson(config); + }; +} diff --git a/server/java/src/main/java/app/config/ShippingOptions.java b/server/java/src/main/java/app/config/ShippingOptions.java new file mode 100644 index 00000000..44178f06 --- /dev/null +++ b/server/java/src/main/java/app/config/ShippingOptions.java @@ -0,0 +1,8 @@ +package app.config; + +public class ShippingOptions { + public String id; + public String label; + public String detail; + public Integer amount; +} diff --git a/server/java/src/main/java/app/fulfillment/Fulfillment.java b/server/java/src/main/java/app/fulfillment/Fulfillment.java new file mode 100644 index 00000000..32074f12 --- /dev/null +++ b/server/java/src/main/java/app/fulfillment/Fulfillment.java @@ -0,0 +1,150 @@ +package app.fulfillment; + +import com.stripe.exception.StripeException; +import com.stripe.model.*; +import com.stripe.net.ApiResource; +import com.google.gson.JsonSyntaxException; +import com.stripe.Stripe; +import com.stripe.exception.SignatureVerificationException; +import com.stripe.net.Webhook; +import java.util.*; + +public class Fulfillment { + + public static Event verifyAndReturn(String payload, String header) throws JsonSyntaxException, SignatureVerificationException { + Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); + + String endpointSecret = System.getenv("STRIPE_WEBHOOK_SECRET"); + + Event event = null; + + if (endpointSecret == null) { + event = ApiResource.GSON.fromJson(payload, Event.class); + } else { + event = Webhook.constructEvent( + payload, header, endpointSecret + ); + } + + return event; + } + + public static Integer fulfill(Event event) throws StripeException { + + PaymentIntent pi = null; + Source source = null; + String paymentIntentId; + + // Switch on the event type to handle the different events we have subscribed to + switch (event.getType()) { + + // Events where the PaymentIntent has succeeded + case "payment_intent.succeeded": + // Get the PaymentIntent out from the event. + pi = (PaymentIntent) event.getDataObjectDeserializer().getObject(); + + // Print a success message + System.out.println("Webhook received! Payment for PaymentIntent " + pi.getId() + " succeeded"); + break; + + case "payment_intent.payment_failed": + // Get the PaymentIntent out from the event. + pi = (PaymentIntent) event.getDataObjectDeserializer().getObject(); + + // Print a failure message + System.out.println("Webhook received! Payment on source " + + pi.getLastPaymentError().getSource().getId() + + " for PaymentIntent " + + pi.getId() + + " failed"); + break; + + case "source.chargeable": + // Get the Source out from the event + source = (Source) event.getDataObjectDeserializer().getObject(); + System.out.println("Webhook received! The source " + + source.getId() + + " is chargeable"); + + // Attempt to get the PaymentIntent associated with this source + paymentIntentId = source.getMetadata().get("paymentIntent"); + + // If there was no PaymentIntent associated with the source, return a 400 + if (paymentIntentId == null) { + System.out.println("Could not find a PaymentIntent in the source.chargeable event " + + event.getId()); + return 400; + } + + // Get the PaymentIntent + pi = PaymentIntent.retrieve(source.getMetadata().get("paymentIntent")); + + // If the PaymentIntent already has a source, return a 403 + if (!(pi.getStatus().equals("requires_payment_method"))) { + return 403; + } + + // Confirm the PaymentIntent using the source id + Map params = new HashMap(); + params.put("source", source.getId()); + pi.confirm(params); + + break; + + case "source.failed": + // Get the Source out from the event. + source = (Source) event.getDataObjectDeserializer().getObject(); + + // Print a failure message + System.out.println("The Source " + + source.getId() + + " failed or timed out."); + + // Attempt to get the PaymentIntent associated with this source + paymentIntentId = source.getMetadata().get("paymentIntent"); + + // If there was no PaymentIntent associated with the source, return a 400 + if (paymentIntentId == null) { + System.out.println("Could not find a PaymentIntent in the source.chargeable event " + + event.getId()); + return 400; + } + + // Get the PaymentIntent + pi = PaymentIntent.retrieve(source.getMetadata().get("paymentIntent")); + + pi.cancel(); + + break; + + case "source.cancelled": + // Get the Source out from the event. + source = (Source) event.getDataObjectDeserializer().getObject(); + + // Print a failure message + System.out.println("The Source " + + source.getId() + + " failed or timed out."); + + // Attempt to get the PaymentIntent associated with this source + paymentIntentId = source.getMetadata().get("paymentIntent"); + + // If there was no PaymentIntent associated with the source, return a 400 + if (paymentIntentId == null) { + System.out.println("Could not find a PaymentIntent in the source.chargeable event " + + event.getId()); + return 400; + } + + // Get the PaymentIntent + pi = PaymentIntent.retrieve(source.getMetadata().get("paymentIntent")); + + pi.cancel(); + + default: + System.out.print("No case to handle events of type " + event.getType()); + } + + return 200; + } +} diff --git a/server/java/src/main/java/app/fulfillment/FulfillmentController.java b/server/java/src/main/java/app/fulfillment/FulfillmentController.java new file mode 100644 index 00000000..c9dfd004 --- /dev/null +++ b/server/java/src/main/java/app/fulfillment/FulfillmentController.java @@ -0,0 +1,29 @@ +package app.fulfillment; + +import com.stripe.net.ApiResource; +import spark.Request; +import spark.Response; +import spark.Route; + +public class FulfillmentController { + + public static Route webhookReceived = (Request request, Response response) -> { + + String payload = request.body(); + String header = request.headers("Stripe-Signature"); + + Integer responseCode = Fulfillment.fulfill(Fulfillment.verifyAndReturn(payload, header)); + + String message; + + if (responseCode == 200) { + message = "success"; + } else { + message = "failure"; + } + + response.status(responseCode); + response.type("application/json"); + return ApiResource.GSON.toJson(message); + }; +} diff --git a/server/java/src/main/java/app/inventory/Inventory.java b/server/java/src/main/java/app/inventory/Inventory.java new file mode 100644 index 00000000..5200cb3f --- /dev/null +++ b/server/java/src/main/java/app/inventory/Inventory.java @@ -0,0 +1,73 @@ +package app.inventory; + +import com.stripe.Stripe; +import com.stripe.exception.StripeException; +import com.stripe.model.Product; +import com.stripe.model.Sku; +import com.stripe.model.ProductCollection; +import com.stripe.model.SkuCollection; +import sun.reflect.generics.reflectiveObjects.NotImplementedException; + +import java.util.*; + + +public class Inventory { + + public static Long calculatePaymentAmount(ArrayList items) throws StripeException { + // Get the Stripe Key + Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); + + Long total = 0L; + Integer quantity = 0; + + for (Map product: items) { + Sku sku = Sku.retrieve(product.get("parent").toString()); + quantity = ((Double) product.get("quantity")).intValue(); + total += sku.getPrice() * quantity; + } + + return total; + } + + public static Long getShippingCost(String id) { + if (id.equals("free")) { + return 0L; + } else if (id.equals("express")) { + return 500L; + } else { + return 0L; + } + } + + public static SkuCollection listSkus(String productId) throws StripeException{ + // Get the Stripe Key + Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); + + Map skuParams = new HashMap(); + skuParams.put("product", productId); + skuParams.put("limit", "3"); + + SkuCollection collection = Sku.list(skuParams); + + return collection; + } + + public static ProductCollection listProducts() throws StripeException{ + // Get the Stripe Key + Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); + + Map productParams = new HashMap(); + productParams.put("limit", "3"); + + ProductCollection collection = Product.list(productParams); + + return collection; + } + + public static Product retrieveProduct(String id) throws StripeException { + // Get the Stripe Key + Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); + + return Product.retrieve(id); + } +} diff --git a/server/java/src/main/java/app/payment/Basket.java b/server/java/src/main/java/app/payment/Basket.java new file mode 100644 index 00000000..71232c7b --- /dev/null +++ b/server/java/src/main/java/app/payment/Basket.java @@ -0,0 +1,9 @@ +package app.payment; + +import java.util.*; + +public class Basket { + String currency; + ArrayList items; + HashMap shippingOption; +} diff --git a/server/java/src/main/java/app/payment/Payment.java b/server/java/src/main/java/app/payment/Payment.java new file mode 100644 index 00000000..5376eb7a --- /dev/null +++ b/server/java/src/main/java/app/payment/Payment.java @@ -0,0 +1,55 @@ +package app.payment; + +import com.stripe.exception.StripeException; +import app.inventory.Inventory; +import com.stripe.model.PaymentIntent; +import com.stripe.*; +import java.util.*; + +public class Payment { + + private PaymentIntent paymentIntent; + + public Payment(Basket basket) throws StripeException { + Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); + + Map paymentintentParams = new HashMap(); + paymentintentParams.put("amount", Inventory.calculatePaymentAmount(basket.items)); + paymentintentParams.put("currency", basket.currency); + List payment_method_types = new ArrayList(); + payment_method_types = Arrays.asList(Optional.ofNullable(System.getenv("PAYMENT_METHODS")).orElse("card").split("\\s*,\\s*")); + paymentintentParams.put( + "payment_method_types", + payment_method_types + ); + + this.paymentIntent = PaymentIntent.create(paymentintentParams); + } + + public PaymentIntent getPaymentIntent() { + return this.paymentIntent; + } + + public static PaymentIntent updateShippingCost(String paymentIntentId, Basket basket) throws StripeException { + + Long amount = Inventory.calculatePaymentAmount(basket.items); + + amount += Inventory.getShippingCost(basket.shippingOption.get("id")); + + PaymentIntent paymentIntent = PaymentIntent.retrieve(paymentIntentId); + + Map paymentintentParams = new HashMap(); + paymentintentParams.put("amount", amount); + + return paymentIntent.update(paymentintentParams); + + } + + public static PaymentIntent getPaymentIntent(String id) throws StripeException { + Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); + + PaymentIntent paymentIntent = PaymentIntent.retrieve(id); + + return paymentIntent; + } +} diff --git a/server/java/src/main/java/app/payment/PaymentController.java b/server/java/src/main/java/app/payment/PaymentController.java new file mode 100644 index 00000000..1243013d --- /dev/null +++ b/server/java/src/main/java/app/payment/PaymentController.java @@ -0,0 +1,66 @@ +package app.payment; + +import com.stripe.model.PaymentIntent; +import com.stripe.net.ApiResource; +import com.google.gson.Gson; +import spark.Request; +import spark.Response; +import spark.Route; + +import java.util.HashMap; +import java.util.Map; + +public class PaymentController { + public static Route createPaymentIntent = (Request request, Response response) -> { + + String requestBody = request.body(); + + Basket basket = ApiResource.GSON.fromJson(requestBody, Basket.class); + + Payment payment = new Payment(basket); + + response.status(200); + response.type("application/json"); + Map wrapper = new HashMap(); + wrapper.put("paymentIntent", payment.getPaymentIntent()); + return ApiResource.GSON.toJson(wrapper); + }; + + public static Route updatePaymentIntent = (Request request, Response response) -> { + + String requestBody = request.body(); + String paymentIntentId = request.params(":id"); + + Basket basket = new Gson().fromJson(requestBody, Basket.class); + + PaymentIntent paymentIntent = Payment.updateShippingCost(paymentIntentId, basket); + + response.status(200); + response.type("application/json"); + + Map wrapper = new HashMap(); + wrapper.put("paymentIntent", paymentIntent); + + return ApiResource.GSON.toJson(wrapper); + + }; + + public static Route getPaymentIntent = (Request request, Response response) -> { + + String paymentIntentId = request.params(":id"); + + PaymentIntent paymentIntent = Payment.getPaymentIntent(paymentIntentId); + + response.status(200); + response.type("application/json"); + + Map status = new HashMap<>(); + status.put("status", paymentIntent.getStatus()); + + Map wrapper = new HashMap(); + wrapper.put("paymentIntent", status); + + return ApiResource.GSON.toJson(wrapper); + + }; +} diff --git a/server/java/src/main/java/app/product/ProductController.java b/server/java/src/main/java/app/product/ProductController.java new file mode 100644 index 00000000..821c8fcc --- /dev/null +++ b/server/java/src/main/java/app/product/ProductController.java @@ -0,0 +1,54 @@ +package app.product; + +import app.inventory.Inventory; +import app.seed.Seed; +import com.stripe.net.ApiResource; +import com.stripe.model.ProductCollection; +import com.stripe.model.SkuCollection; +import com.stripe.model.Product; +import spark.Request; +import spark.Response; +import spark.Route; +import java.util.*; + +public class ProductController { + + public static Route getProducts = (Request request, Response response) -> { + + ProductCollection products = Inventory.listProducts(); + + if (products.getData().size() > 0) { + response.status(200); + response.type("application/json"); + return ApiResource.GSON.toJson(products); + } else { + Seed.seed(); + products = Inventory.listProducts(); + response.status(200); + response.type("application/json"); + return ApiResource.GSON.toJson(products); + } + }; + + public static Route getProduct = (Request request, Response response) -> { + + Product product = Inventory.retrieveProduct(request.params(":id")); + + response.status(200); + response.type("application/json"); + return ApiResource.GSON.toJson(product); + + }; + + public static Route getSKUsForProduct = (Request request, Response response) -> { + + String productId = request.params(":id"); + + SkuCollection skus = Inventory.listSkus(productId); + + response.status(200); + response.type("application/json"); + return ApiResource.GSON.toJson(skus); + + }; +} diff --git a/server/java/src/main/java/app/seed/Seed.java b/server/java/src/main/java/app/seed/Seed.java new file mode 100644 index 00000000..eb570cf3 --- /dev/null +++ b/server/java/src/main/java/app/seed/Seed.java @@ -0,0 +1,124 @@ +package app.seed; + +import com.stripe.*; +import com.stripe.exception.InvalidRequestException; +import com.stripe.model.Product; +import com.stripe.model.Sku; +import com.stripe.exception.StripeException; +import java.util.*; + +public class Seed { + public static void seed() throws StripeException{ + try { + Seed.seedProducts(); + } catch (InvalidRequestException e) { + System.out.println("Products already exist."); + } + try { + Seed.seedSKUs(); + } catch (InvalidRequestException e) { + System.out.println("SKUs already exist."); + } + } + + private static void seedProducts() throws StripeException { + Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); + + List productsToAdd = new ArrayList(); + + //Add Increment Magazine + Map incrementMagazine = new HashMap(); + incrementMagazine.put("id", "increment"); + incrementMagazine.put("type", "good"); + incrementMagazine.put("name", "Increment Magazine"); + ArrayList incAttrs = new ArrayList(); + incAttrs.add("issue"); + incrementMagazine.put("attributes", incAttrs); + + productsToAdd.add(incrementMagazine); + + //Add Stripe Pins + Map stripePins = new HashMap(); + stripePins.put("id", "pins"); + stripePins.put("type", "good"); + stripePins.put("name", "Stripe Pins"); + ArrayList pinsAttrs = new ArrayList(); + pinsAttrs.add("set"); + stripePins.put("attributes", pinsAttrs); + + productsToAdd.add(stripePins); + + //Add Stripe Pins + Map stripeShirt = new HashMap(); + stripeShirt.put("id", "shirt"); + stripeShirt.put("type", "good"); + stripeShirt.put("name", "Stripe Shirt"); + ArrayList shirtAttrs = new ArrayList(); + shirtAttrs.add("size"); + shirtAttrs.add("gender"); + stripeShirt.put("attributes", shirtAttrs); + + productsToAdd.add(stripeShirt); + + for (Map product: productsToAdd) { + Product.create(product); + } + } + + private static void seedSKUs() throws StripeException{ + Stripe.apiKey = System.getenv("STRIPE_SECRET_KEY"); + + List skusToAdd = new ArrayList(); + + //Increment Magazine + Map incrementSkuParams = new HashMap(); + incrementSkuParams.put("id", "increment-03"); + incrementSkuParams.put("product", "increment"); + incrementSkuParams.put("price", 399); + incrementSkuParams.put("currency", "usd"); + Map incrementAttrParams = new HashMap(); + incrementAttrParams.put("issue", "Issue #3 “Development”"); + incrementSkuParams.put("attributes", incrementAttrParams); + Map incrementParams = new HashMap(); + incrementParams.put("type", "infinite"); + incrementSkuParams.put("inventory", incrementParams); + + skusToAdd.add(incrementSkuParams); + + //Stripe Pin + Map pinsSkuParams = new HashMap(); + pinsSkuParams.put("id", "pins-collector"); + pinsSkuParams.put("product", "pins"); + pinsSkuParams.put("price", 799); + pinsSkuParams.put("currency", "usd"); + Map pinsAttrParams = new HashMap(); + pinsAttrParams.put("set", "Collector Set"); + pinsSkuParams.put("attributes", pinsAttrParams); + Map pinsParams = new HashMap(); + pinsParams.put("type", "finite"); + pinsParams.put("quantity", 500); + pinsSkuParams.put("inventory", pinsParams); + + skusToAdd.add(pinsSkuParams); + + //Stripe Shirt + Map shirtSkuParams = new HashMap(); + shirtSkuParams.put("id", "shirt-small-woman"); + shirtSkuParams.put("product", "shirt"); + shirtSkuParams.put("price", 999); + shirtSkuParams.put("currency", "usd"); + Map shirtAttrParams = new HashMap(); + shirtAttrParams.put("size", "Small Standard"); + shirtAttrParams.put("gender", "Woman"); + shirtSkuParams.put("attributes", shirtAttrParams); + Map shirtParams = new HashMap(); + shirtParams.put("type", "infinite"); + shirtSkuParams.put("inventory", shirtParams); + + skusToAdd.add(shirtSkuParams); + + for (Map sku: skusToAdd) { + Sku.create(sku); + } + } +}