├── src ├── main │ ├── resources │ │ └── DigiCertSHA2ExtendedValidationServerCA.crt │ └── java │ │ └── com │ │ └── osiris │ │ ├── payhook │ │ ├── PaymentProcessor.java │ │ ├── utils │ │ │ ├── UtilsLists.java │ │ │ ├── UtilsMap.java │ │ │ ├── UtilsTime.java │ │ │ ├── ThreadWithCounter.java │ │ │ └── Converter.java │ │ ├── exceptions │ │ │ ├── ParseBodyException.java │ │ │ ├── ParseHeaderException.java │ │ │ ├── InvalidChangeException.java │ │ │ └── WebHookValidationException.java │ │ ├── stripe │ │ │ ├── StripePrice.java │ │ │ └── UtilsStripe.java │ │ ├── paypal │ │ │ ├── Constants.java │ │ │ ├── PayPalPlan.java │ │ │ ├── PayPalSubscription.java │ │ │ ├── PaypalWebhookEventHeader.java │ │ │ ├── PaypalWebhookEvent.java │ │ │ ├── UtilsPayPal.java │ │ │ ├── UtilsPayPalJson.java │ │ │ └── PayPalUtils.java │ │ └── Subscription.java │ │ └── jsqlgen │ │ ├── payhook │ │ ├── Database.java │ │ ├── WebhookEndpoint.java │ │ ├── PendingPaymentCancel.java │ │ └── PaymentWarning.java │ │ └── payhook_structure.json └── test │ └── java │ └── com │ └── osiris │ ├── payhook │ ├── SubscriptionTest.java │ ├── DatabaseTest.java │ ├── ExampleConstants.java │ ├── SQLTestServer.java │ ├── TestPayment.java │ └── MainTest.java │ └── jsqlgen │ └── payhook │ └── PaymentTest.java ├── .gitignore ├── README_TODO.md ├── LICENSE ├── DESIGN.md └── pom.xml /src/main/resources/DigiCertSHA2ExtendedValidationServerCA.crt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Osiris-Team/PayHook/HEAD/src/main/resources/DigiCertSHA2ExtendedValidationServerCA.crt -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/PaymentProcessor.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook; 2 | 3 | public enum PaymentProcessor { 4 | PAYPAL, 5 | BRAINTREE, 6 | STRIPE 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/utils/UtilsLists.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.utils; 2 | 3 | import java.util.List; 4 | 5 | public class UtilsLists { 6 | public static boolean containsIgnoreCase(List stringList, String s) { 7 | for (String s0 : 8 | stringList) { 9 | if (s0.equalsIgnoreCase(s)) return true; 10 | } 11 | return false; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target/ 2 | **/test-credentials.yml 3 | /db 4 | /headless-browser 5 | 6 | # IDEA 7 | .idea/ 8 | *.iml 9 | 10 | # Compiled class file 11 | *.class 12 | 13 | # Log file 14 | *.log 15 | 16 | # BlueJ files 17 | *.ctxt 18 | 19 | # Mobile Tools for Java (J2ME) 20 | .mtj.tmp/ 21 | 22 | # Package Files # 23 | *.jar 24 | *.war 25 | *.nar 26 | *.ear 27 | *.zip 28 | *.tar.gz 29 | *.rar 30 | 31 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 32 | hs_err_pid* 33 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/utils/UtilsMap.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.utils; 2 | 3 | import java.util.Map; 4 | 5 | public class UtilsMap { 6 | public String mapToString(Map map) { 7 | StringBuilder sb = new StringBuilder(); 8 | map.forEach((key, val) -> { 9 | sb.append(key).append(" = ").append(val); 10 | }); 11 | return sb.toString(); 12 | } 13 | 14 | public String mapToStringWithLineBreaks(Map map) { 15 | StringBuilder sb = new StringBuilder(); 16 | map.forEach((key, val) -> { 17 | sb.append(key).append(" = ").append(val).append("\n"); 18 | }); 19 | return sb.toString(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/exceptions/ParseBodyException.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.exceptions; 2 | 3 | public class ParseBodyException extends Exception { 4 | 5 | public ParseBodyException() { 6 | } 7 | 8 | public ParseBodyException(String message) { 9 | super(message); 10 | } 11 | 12 | public ParseBodyException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public ParseBodyException(Throwable cause) { 17 | super(cause); 18 | } 19 | 20 | public ParseBodyException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 21 | super(message, cause, enableSuppression, writableStackTrace); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/exceptions/ParseHeaderException.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.exceptions; 2 | 3 | public class ParseHeaderException extends Exception { 4 | public ParseHeaderException() { 5 | } 6 | 7 | public ParseHeaderException(String message) { 8 | super(message); 9 | } 10 | 11 | public ParseHeaderException(String message, Throwable cause) { 12 | super(message, cause); 13 | } 14 | 15 | public ParseHeaderException(Throwable cause) { 16 | super(cause); 17 | } 18 | 19 | public ParseHeaderException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 20 | super(message, cause, enableSuppression, writableStackTrace); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/exceptions/InvalidChangeException.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.exceptions; 2 | 3 | public class InvalidChangeException extends Exception { 4 | 5 | public InvalidChangeException() { 6 | } 7 | 8 | public InvalidChangeException(String message) { 9 | super(message); 10 | } 11 | 12 | public InvalidChangeException(String message, Throwable cause) { 13 | super(message, cause); 14 | } 15 | 16 | public InvalidChangeException(Throwable cause) { 17 | super(cause); 18 | } 19 | 20 | public InvalidChangeException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 21 | super(message, cause, enableSuppression, writableStackTrace); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/exceptions/WebHookValidationException.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.exceptions; 2 | 3 | /** 4 | * Thrown when the webhook couldn't be validated. 5 | */ 6 | public class WebHookValidationException extends Exception { 7 | public WebHookValidationException() { 8 | } 9 | 10 | public WebHookValidationException(String message) { 11 | super(message); 12 | } 13 | 14 | public WebHookValidationException(String message, Throwable cause) { 15 | super(message, cause); 16 | } 17 | 18 | public WebHookValidationException(Throwable cause) { 19 | super(cause); 20 | } 21 | 22 | public WebHookValidationException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { 23 | super(message, cause, enableSuppression, writableStackTrace); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/stripe/StripePrice.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.stripe; 2 | 3 | public class StripePrice { 4 | private long priceInSmallestCurrency; 5 | private String currency; 6 | 7 | public StripePrice(long priceInSmallestCurrency, String currency) { 8 | this.priceInSmallestCurrency = priceInSmallestCurrency; 9 | this.currency = currency; 10 | } 11 | 12 | public long getPriceInSmallestCurrency() { 13 | return priceInSmallestCurrency; 14 | } 15 | 16 | public void setPriceInSmallestCurrency(long priceInSmallestCurrency) { 17 | this.priceInSmallestCurrency = priceInSmallestCurrency; 18 | } 19 | 20 | public String getCurrency() { 21 | return currency; 22 | } 23 | 24 | public void setCurrency(String currency) { 25 | this.currency = currency; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /README_TODO.md: -------------------------------------------------------------------------------- 1 | ## Upgrade Notes 2 | If you are on a version mentioned below you will need to follow the respective steps to upgrade 3 | to the latest version. You might need to follow more steps the older your current version is. 4 | 5 | 6 | ### Your version <= 4.9.15 7 | - Added new field Payment.chargeRefunded and new refund logic. Previously, to create a refund, you would need to 8 | subtract the amount to refund from Payment.charge. Now, you need to add the amount to Payment.chargeRefunded instead. 9 | Meaning Payment.charge will never change and always display the original price, thus when calculating the sum of all payments for 10 | example, keep in mind that Payment.charRefunded might be != 0. 11 | - PayPal (MAYBE) seems to give a lot of time if the user has no funds to complete payments. 12 | Thus we may get a larger payment sometime later (if the expired events by PayHook are ignored and the subscription not cancelled). -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Osiris Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/paypal/Constants.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.paypal; 2 | 3 | public final class Constants { 4 | 5 | // PayPal webhook transmission ID HTTP request header 6 | public static final String PAYPAL_HEADER_TRANSMISSION_ID = "PAYPAL-TRANSMISSION-ID"; 7 | // PayPal webhook transmission time HTTP request header 8 | public static final String PAYPAL_HEADER_TRANSMISSION_TIME = "PAYPAL-TRANSMISSION-TIME"; 9 | // PayPal webhook transmission signature HTTP request header 10 | public static final String PAYPAL_HEADER_TRANSMISSION_SIG = "PAYPAL-TRANSMISSION-SIG"; 11 | // PayPal webhook certificate URL HTTP request header 12 | public static final String PAYPAL_HEADER_CERT_URL = "PAYPAL-CERT-URL"; 13 | // PayPal webhook authentication algorithm HTTP request header 14 | public static final String PAYPAL_HEADER_AUTH_ALGO = "PAYPAL-AUTH-ALGO"; 15 | // Trust Certificate Location to be used to validate webhook certificates 16 | public static final String PAYPAL_TRUST_CERT_URL = "webhook.trustCert"; 17 | // Default Trust Certificate that comes packaged with SDK. 18 | public static final String PAYPAL_TRUST_DEFAULT_CERT = "DigiCertSHA2ExtendedValidationServerCA.crt"; 19 | 20 | private Constants() { 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/paypal/PayPalPlan.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Osiris Team 3 | * All rights reserved. 4 | * 5 | * This software is copyrighted work licensed under the terms of the 6 | * AutoPlug License. Please consult the file "LICENSE" for details. 7 | */ 8 | 9 | package com.osiris.payhook.paypal; 10 | 11 | public class PayPalPlan { 12 | private final PayPalUtils context; 13 | private final String planId; 14 | private final String productId; 15 | private final String name; 16 | private final String description; 17 | private final Status status; 18 | 19 | public PayPalPlan(PayPalUtils context, String planId, String productId, String name, String description, Status status) { 20 | this.context = context; 21 | this.planId = planId; 22 | this.productId = productId; 23 | this.name = name; 24 | this.description = description; 25 | this.status = status; 26 | } 27 | 28 | /** 29 | * Creates a subscription for this plan and returns it. 30 | */ 31 | 32 | public String getPlanId() { 33 | return planId; 34 | } 35 | 36 | public String getProductId() { 37 | return productId; 38 | } 39 | 40 | public String getName() { 41 | return name; 42 | } 43 | 44 | public String getDescription() { 45 | return description; 46 | } 47 | 48 | public Status getStatus() { 49 | return status; 50 | } 51 | 52 | public enum Status { 53 | CREATED, ACTIVE, INACTIVE 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/osiris/payhook/SubscriptionTest.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook; 2 | 3 | import com.osiris.jsqlgen.payhook.Payment; 4 | import com.osiris.payhook.utils.UtilsTime; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | import static org.junit.jupiter.api.Assertions.assertFalse; 11 | import static org.junit.jupiter.api.Assertions.assertTrue; 12 | 13 | class SubscriptionTest { 14 | @Test 15 | void test() { 16 | long now = System.currentTimeMillis(); 17 | int interval = Payment.Interval.MONTHLY; 18 | long timestampCreated = now; 19 | long timestampExpires = now + 1000; 20 | long timestampAuthorized = now; 21 | long timestampCancelled = 0; 22 | long timestampRefunded = 0; 23 | Payment payment = new Payment(0, "user", 100, "EUR", interval, "", 0, "my-sub", 24 | 1, timestampCreated, timestampExpires, timestampAuthorized, timestampCancelled, timestampRefunded, null, "stripe-id", null, null, null, null); 25 | List payments = new ArrayList<>(); 26 | payments.add(payment); 27 | Subscription sub = new Subscription(payments); 28 | assertTrue(sub.getLastPayment() == payment); 29 | assertTrue(sub.getMillisLeft() >= UtilsTime.MS_MONTH - UtilsTime.MS_MINUTE); 30 | assertTrue(sub.getMillisLeftWithPuffer() > sub.getMillisLeft() && sub.getMillisLeftWithPuffer() > UtilsTime.MS_MONTH); 31 | assertTrue(sub.isTimeLeft()); 32 | assertFalse(sub.isCancelled()); 33 | } 34 | } -------------------------------------------------------------------------------- /src/test/java/com/osiris/payhook/DatabaseTest.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook; 2 | 3 | import ch.vorburger.exec.ManagedProcessException; 4 | import com.osiris.jsqlgen.payhook.Database; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | public class DatabaseTest { 11 | private static boolean isInit = false; 12 | public static synchronized void init() throws ManagedProcessException { 13 | if(isInit) return; 14 | System.out.println("Starting database..."); 15 | SQLTestServer sqlTestServer = SQLTestServer.buildAndRun(); 16 | Database.url = sqlTestServer.getUrl(); 17 | Database.username = "root"; 18 | Database.password = ""; 19 | System.out.println("Url: " + Database.url); 20 | System.out.println("OK!"); 21 | isInit = true; 22 | } 23 | @Test 24 | void test() throws Exception { 25 | init(); 26 | 27 | for (TestPayment p : TestPayment.get()) { 28 | TestPayment.delete(p); 29 | } 30 | assertTrue(TestPayment.get().isEmpty()); 31 | 32 | TestPayment testPayment = TestPayment.create(); 33 | testPayment.age = 1; 34 | testPayment.name = "My first payment."; 35 | TestPayment.add(testPayment); 36 | 37 | for (TestPayment p : TestPayment.get()) { 38 | System.out.println(p.id + " " + p.age + " " + p.name); 39 | } 40 | assertEquals(1, TestPayment.get().size()); 41 | assertEquals(1, TestPayment.get("age = 1").size()); 42 | assertEquals(0, TestPayment.get("age = 0").size()); 43 | 44 | testPayment.name = "New payment name!"; 45 | TestPayment.update(testPayment); 46 | assertEquals(testPayment.name, TestPayment.get().get(0).name); 47 | 48 | TestPayment.delete(testPayment); 49 | assertTrue(TestPayment.get().isEmpty()); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/paypal/PayPalSubscription.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Osiris Team 3 | * All rights reserved. 4 | * 5 | * This software is copyrighted work licensed under the terms of the 6 | * AutoPlug License. Please consult the file "LICENSE" for details. 7 | */ 8 | 9 | package com.osiris.payhook.paypal; 10 | 11 | public class PayPalSubscription { 12 | private final PayPalPlan payPalPlan; 13 | private final String id; 14 | private final Status status; 15 | private final String approveUrl; // Example: https://www.paypal.com/webapps/billing/subscriptions?ba_token=BA-2M539689T3856352J 16 | private final String editUrl; // Example: https://api-m.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G 17 | private final String selfUrl; // Example: https://api-m.paypal.com/v1/billing/subscriptions/I-BW452GLLEP1G 18 | 19 | public PayPalSubscription(PayPalPlan payPalPlan, String id, Status status, String approveUrl, String editUrl, String selfUrl) { 20 | this.payPalPlan = payPalPlan; 21 | this.id = id; 22 | this.status = status; 23 | this.approveUrl = approveUrl; 24 | this.editUrl = editUrl; 25 | this.selfUrl = selfUrl; 26 | } 27 | 28 | public PayPalPlan getPlan() { 29 | return payPalPlan; 30 | } 31 | 32 | public String getId() { 33 | return id; 34 | } 35 | 36 | public Status getStatus() { 37 | return status; 38 | } 39 | 40 | public String getApproveUrl() { 41 | return approveUrl; 42 | } 43 | 44 | public String getEditUrl() { 45 | return editUrl; 46 | } 47 | 48 | public String getSelfUrl() { 49 | return selfUrl; 50 | } 51 | 52 | public enum Status { 53 | APPROVAL_PENDING, // The subscription is created but not yet approved by the buyer. 54 | APPROVED, // The buyer has approved the subscription. 55 | ACTIVE, // The subscription is active. 56 | SUSPENDED, // The subscription is suspended. 57 | CANCELLED, // The subscription is cancelled. 58 | EXPIRED, // The subscription is expired. 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/paypal/PaypalWebhookEventHeader.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.paypal; 2 | 3 | public class PaypalWebhookEventHeader { 4 | private final String transmissionId; 5 | private final String timestamp; 6 | private final String transmissionSignature; 7 | private final String authAlgorithm; 8 | private final String certUrl; 9 | private String webhookId; 10 | 11 | public PaypalWebhookEventHeader(String transmissionId, String timestamp, String transmissionSignature, String authAlgorithm, String certUrl, 12 | String webhookId) { 13 | this.transmissionId = transmissionId; 14 | this.timestamp = timestamp; 15 | this.transmissionSignature = transmissionSignature; 16 | this.authAlgorithm = authAlgorithm; 17 | this.certUrl = certUrl; 18 | this.webhookId = webhookId; 19 | } 20 | 21 | /** 22 | * The unique ID of the HTTP transmission. 23 | * Contained in PAYPAL-TRANSMISSION-ID header of the notification message. 24 | */ 25 | public String getTransmissionId() { 26 | return transmissionId; 27 | } 28 | 29 | /** 30 | * The date and time when the HTTP message was transmitted. 31 | * Contained in PAYPAL-TRANSMISSION-TIME header of the notification message. 32 | */ 33 | public String getTimestamp() { 34 | return timestamp; 35 | } 36 | 37 | 38 | public String getWebhookId() { 39 | return webhookId; 40 | } 41 | 42 | /** 43 | * See {@link PaypalWebhookEventHeader#getWebhookId()} for details. 44 | */ 45 | public void setWebhookId(String webhookId) { 46 | this.webhookId = webhookId; 47 | } 48 | 49 | /** 50 | * The PayPal-generated asymmetric signature. 51 | */ 52 | public String getTransmissionSignature() { 53 | return transmissionSignature; 54 | } 55 | 56 | /** 57 | * The algorithm that PayPal used to generate the signature and that you can use to verify the signature. 58 | */ 59 | public String getAuthAlgorithm() { 60 | return authAlgorithm; 61 | } 62 | 63 | /** 64 | * The X509 public key certificate. 65 | * Download the certificate from this URL and use it to verify the signature. 66 | */ 67 | public String getCertUrl() { 68 | return certUrl; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/osiris/jsqlgen/payhook/PaymentTest.java: -------------------------------------------------------------------------------- 1 | package com.osiris.jsqlgen.payhook; 2 | 3 | import ch.vorburger.exec.ManagedProcessException; 4 | import com.osiris.payhook.DatabaseTest; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.junit.jupiter.api.Assertions.assertEquals; 8 | 9 | class PaymentTest { 10 | 11 | private static long now = System.currentTimeMillis(); 12 | 13 | @Test 14 | void getSubscriptionPaymentsForUser() throws ManagedProcessException { 15 | DatabaseTest.init(); 16 | Payment.whereUserId().is("test").remove(); 17 | Payment.createAndAdd("test", 100, "EUR", Payment.Interval.NONE); 18 | Payment p = Payment.createAndAdd("test", 100, "EUR", Payment.Interval.MONTHLY); 19 | assertEquals(0, Payment.getUserSubscriptionPayments("test").size()); 20 | p.paypalSubscriptionId = "not-null"; 21 | Payment.update(p); 22 | assertEquals(1, Payment.getUserSubscriptionPayments("test").size()); 23 | } 24 | 25 | @Test 26 | void getPendingPayments() throws ManagedProcessException { 27 | DatabaseTest.init(); 28 | Payment.whereUserId().is("test").remove(); 29 | Payment p = Payment.createAndAdd("test", 100, "EUR", Payment.Interval.NONE); 30 | p.timestampCreated = now - 1000; 31 | Payment.update(p); 32 | assertEquals(1, Payment.getUserPendingPayments("test").size()); 33 | } 34 | 35 | @Test 36 | void getAuthorizedPayments() throws ManagedProcessException { 37 | DatabaseTest.init(); 38 | Payment.whereUserId().is("test").remove(); 39 | Payment p = Payment.createAndAdd("test", 100, "EUR", Payment.Interval.NONE); 40 | p.timestampAuthorized = now - 1000; 41 | Payment.update(p); 42 | assertEquals(1, Payment.getUserAuthorizedPayments("test").size()); 43 | } 44 | 45 | @Test 46 | void getCancelledPayments() throws ManagedProcessException { 47 | DatabaseTest.init(); 48 | Payment.whereUserId().is("test").remove(); 49 | Payment p = Payment.createAndAdd("test", 100, "EUR", Payment.Interval.NONE); 50 | p.timestampCancelled = now - 1000; 51 | Payment.update(p); 52 | assertEquals(1, Payment.getUserCancelledPayments("test").size()); 53 | } 54 | 55 | @Test 56 | void getRefundedPayments() throws ManagedProcessException { 57 | DatabaseTest.init(); 58 | Payment.whereUserId().is("test").remove(); 59 | Payment p = Payment.createAndAdd("test", 100, "EUR", Payment.Interval.NONE); 60 | p.timestampRefunded = now - 1000; 61 | Payment.update(p); 62 | assertEquals(1, Payment.getUserRefundedPayments("test").size()); 63 | } 64 | } -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/utils/UtilsTime.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Velocity Contributors 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU General Public License as published by 6 | * the Free Software Foundation, either version 3 of the License, or 7 | * (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU General Public License 15 | * along with this program. If not, see . 16 | * 17 | */ 18 | 19 | package com.osiris.payhook.utils; 20 | 21 | public class UtilsTime { 22 | public static final long MS_YEAR = 31556952000L; 23 | public static final long MS_MONTH = 2629800000L; 24 | public static final int MS_DAY = 86400000; 25 | public static final int MS_HOUR = 86400000; 26 | public static final int MS_MINUTE = 86400000; 27 | public static final int MS_SECONDS = 86400000; 28 | 29 | public String getFormattedString(long ms) { 30 | StringBuilder s = new StringBuilder(); 31 | // years, months, days, hours, minutes, seconds 32 | long years, months, days, hours, minutes, seconds; 33 | 34 | 35 | if (ms > MS_YEAR) { 36 | years = ms / MS_YEAR; 37 | if (years >= 1) { 38 | s.append(years + "y"); 39 | ms -= years * MS_YEAR; 40 | } 41 | } 42 | if (ms > MS_MONTH) { 43 | months = ms / MS_MONTH; 44 | if (months >= 1) { 45 | s.append(months + "mo"); 46 | ms -= months * MS_MONTH; 47 | } 48 | } 49 | if (ms > MS_DAY) { 50 | days = ms / MS_DAY; 51 | if (days >= 1) { 52 | s.append(days + "d"); 53 | ms -= days * MS_DAY; 54 | } 55 | } 56 | if (ms > MS_HOUR) { 57 | hours = ms / MS_HOUR; 58 | if (hours >= 1) { 59 | s.append(hours + "h"); 60 | ms -= hours * MS_HOUR; 61 | } 62 | } 63 | if (ms > MS_MINUTE) { 64 | minutes = ms / MS_MINUTE; 65 | if (minutes >= 1) { 66 | s.append(minutes + "mi"); 67 | ms -= minutes * MS_MINUTE; 68 | } 69 | } 70 | if (ms > MS_SECONDS) { 71 | seconds = ms / MS_SECONDS; 72 | if (seconds >= 1) { 73 | s.append(seconds + "s"); 74 | ms -= seconds * MS_SECONDS; 75 | } 76 | } 77 | return s.toString(); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # Design 2 | PayHooks' main goal is to achieve a simple way of handling payments in Java. 3 | This file explains the relevant classes and how they work together, in detail. 4 | 5 | ## PayHook 6 | This class contains static methods for initialising the internal 7 | payment processor specific libraries, as well as creating pending payments 8 | your customers must authorize by visiting a checkout page and completing 9 | the necessary steps (like logging in with their PayPal account or entering 10 | credit card details). 11 | 12 | This class also has methods for handling payment processor webhook events. 13 | These are critical at the moment to PayHooks functionality. 14 | Webhook events make it easier to deal with recurring payments (subscriptions) 15 | and also help to avoid hitting payment processor API rate-limits. 16 | 17 | ## Payment 18 | The payment object represents a payment made by a customer or to a customer 19 | (a refund for example). It can be in multiple states, like pending 20 | (not paid/authorized yet), completed/authorized or cancelled. Methods are available to distinguish 21 | between these states and payment type. 22 | 23 | This is a database object which means that static methods are available 24 | to interact with the Payment table with which you can 25 | list/retrieve all payments for example. 26 | 27 | ## PaymentWarning (todo) 28 | If something didn't go to plan when receiving a webhook event related 29 | to a payment, then a PaymentWarning object is created that contains a paymentId 30 | and message, and added to the database. 31 | These warnings should give an insight in what exactly went wrong and 32 | usually shouldn't be threaded lightly. 33 | 34 | Note that if a paymentId couldn't be determined (which usually means that 35 | the issue occurred before even being able to assign it to a paymentId) 36 | an exception is thrown instead in `PayHook.receiveWebhookEvent(...)` method. 37 | 38 | This is a database object which means that static methods are available 39 | to interact with the PaymentWarning table with which you can 40 | list/retrieve all PaymentWarnings for example. 41 | 42 | ## Product 43 | It contains details about the product a customer can buy (make payments for) 44 | and its details get synchronized across payment processors when changes 45 | are detected. Thus changes to a products' details shouldn't be done 46 | over the payment processors web panels, but to the product object 47 | itself inside Java code. The `PayHook.putProduct(...)` method then handles 48 | synchronisation. 49 | 50 | This is a database object which means that static methods are available 51 | to interact with the Product table with which you can 52 | list/retrieve all Products for example. 53 | 54 | ## Subscription 55 | Helper class to map multiple payments to a single subscription object. 56 | Provides additional subscription specific methods that make it easier 57 | to deal with payments for subscriptions. 58 | 59 | Not a database object, but still has some static methods 60 | for retrieving subscriptions. -------------------------------------------------------------------------------- /src/test/java/com/osiris/payhook/ExampleConstants.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook; 2 | 3 | import com.osiris.jsqlgen.payhook.Payment; 4 | import com.osiris.jsqlgen.payhook.Product; 5 | 6 | public class ExampleConstants { 7 | public static Product pCoolCookie; 8 | public static Product pCoolSubscription; 9 | 10 | // Insert the below somewhere where it gets ran once. 11 | // For example in the static constructor of a Constants class of yours, like shown here, or in your main method. 12 | static { 13 | try { 14 | PayHook.init( 15 | "Brand-Name", 16 | "db_url", 17 | "db_name", 18 | "db_username", 19 | "db_password", 20 | true, 21 | "https://my-shop.com/payment/success", 22 | "https://my-shop.com/payment/cancel"); 23 | 24 | PayHook.initBraintree("merchant_id", "public_key", "private_key", "https://my-shop.com/braintree-hook"); 25 | PayHook.initStripe("secret_key", "https://my-shop.com/stripe-hook"); 26 | 27 | pCoolCookie = PayHook.putProduct(0, 500, "EUR", "Cool-Cookie", "A really yummy cookie.", Payment.Interval.NONE); 28 | pCoolSubscription = PayHook.putProduct(1, 999, "EUR", "Cool-Subscription", "A really creative description.", Payment.Interval.MONTHLY); 29 | 30 | PayHook.onPaymentAuthorized.addAction(payment -> { 31 | // Additional backend business logic for all payments in here. 32 | // Gets executed every time a payment is authorized/completed. 33 | // If something goes wrong in here a RuntimeException is thrown. 34 | }); 35 | 36 | PayHook.onPaymentCancelled.addAction(payment -> { 37 | // Additional backend business logic for all payments in here. 38 | // Gets executed every time a payment was cancelled. 39 | // If something goes wrong in here a RuntimeException is thrown. 40 | }); 41 | } catch (Exception e) { 42 | throw new RuntimeException(e); 43 | } 44 | } 45 | 46 | /** 47 | * This can be anywhere in your application. 48 | */ 49 | void onAnotherBuyBtnClick() throws Exception { 50 | Payment payment = PayHook.expectPayment("USER_ID", pCoolSubscription, PaymentProcessor.STRIPE, 51 | authorizedPayment -> { 52 | // Insert ONLY additional UI code here (make sure to have access to the UI thread). 53 | // Code that does backend, aka important stuff does not belong here! 54 | }, cancelledPayment -> { 55 | // Insert ONLY additional UI code here (make sure to have access to the UI thread). 56 | // Code that does backend, aka important stuff does not belong here! 57 | }); 58 | // Forward your user to payment.url to complete/authorize the payment here... 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/osiris/payhook/SQLTestServer.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook; 2 | 3 | import ch.vorburger.exec.ManagedProcessException; 4 | import ch.vorburger.mariadb4j.DB; 5 | import ch.vorburger.mariadb4j.DBConfigurationBuilder; 6 | 7 | import java.io.File; 8 | 9 | public class SQLTestServer { 10 | private final DBConfigurationBuilder properties; 11 | private final DB server; 12 | private boolean running = false; 13 | private String name; 14 | private String url; 15 | 16 | /** 17 | * Creates and opens a new MySQL server 18 | * (or loads already existing) at the default path: 'user dir'/db 19 | */ 20 | private SQLTestServer() throws ManagedProcessException { 21 | this(getDefaultSQLProps()); 22 | } 23 | 24 | private SQLTestServer(DBConfigurationBuilder props) throws ManagedProcessException { 25 | properties = props; 26 | server = DB.newEmbeddedDB(props.build()); 27 | server.start(); 28 | running = true; 29 | } 30 | 31 | public static SQLTestServer buildAndRun() throws ManagedProcessException { 32 | final String name = "test"; // MUST BE TEST, see: https://github.com/vorburger/MariaDB4j#how-java 33 | DBConfigurationBuilder configBuilder = DBConfigurationBuilder.newBuilder(); 34 | configBuilder.setPort(3306); // OR, default: setPort(0); => autom. detect free port 35 | configBuilder.setDeletingTemporaryBaseAndDataDirsOnShutdown(false); 36 | configBuilder.setBaseDir(System.getProperty("user.dir") + File.separator + 37 | "db" + File.separator + "base"); 38 | configBuilder.setDataDir(System.getProperty("user.dir") + File.separator + 39 | "db" + File.separator + "databases" + File.separator + File.separator + name); // just an example 40 | configBuilder.setLibDir(System.getProperty("user.dir") + File.separator + 41 | "db" + File.separator + "libs"); 42 | 43 | SQLTestServer server = new SQLTestServer(configBuilder); 44 | server.setName(name); 45 | server.setUrl("jdbc:mysql://localhost/" + name);// MUST BE TEST, see: https://github.com/vorburger/MariaDB4j#how-java 46 | return server; 47 | } 48 | 49 | private static DBConfigurationBuilder getDefaultSQLProps() { 50 | DBConfigurationBuilder configBuilder = DBConfigurationBuilder.newBuilder(); 51 | configBuilder.setPort(3306); // OR, default: setPort(0); => autom. detect free port 52 | configBuilder.setDataDir(System.getProperty("user.dir") + File.separator + "db" + File.separator + "testDB"); // just an example 53 | return configBuilder; 54 | } 55 | 56 | public String getName() { 57 | return name; 58 | } 59 | 60 | public void setName(String name) { 61 | this.name = name; 62 | } 63 | 64 | public String getUrl() { 65 | return url; 66 | } 67 | 68 | public void setUrl(String url) { 69 | this.url = url; 70 | } 71 | 72 | public DBConfigurationBuilder getProperties() { 73 | return properties; 74 | } 75 | 76 | public DB getServer() { 77 | return server; 78 | } 79 | 80 | public boolean isRunning() { 81 | return running; 82 | } 83 | } -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/utils/ThreadWithCounter.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.utils; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | public class ThreadWithCounter extends Thread { 6 | public AtomicInteger counter = new AtomicInteger(0); 7 | public Runnable runnable = null; 8 | 9 | @Override 10 | public void run() { 11 | super.run(); 12 | if (runnable != null) runnable.run(); 13 | } 14 | /* 15 | TODO Support this: 16 | ThreadWithCounter threadCheckIfPaid = new ThreadWithCounter(); // Only ran if no webhook 17 | threadCheckIfPaid.runnable = () -> { 18 | try{ 19 | long msToSleepStart = 10000; // 10sek 20 | long msToSleep = msToSleepStart; 21 | long msSleptTotal = 0; 22 | while (true){ 23 | Thread.sleep(msToSleep); 24 | msSleptTotal += msToSleep; 25 | 26 | Timestamp now = new Timestamp(System.currentTimeMillis()); 27 | Map> paypalOrders = new HashMap<>(); 28 | // CAPTURE PAYPAL SUBSCRIPTIONS: 29 | for (Payment pendingPayment : database.getPendingPayments()) { 30 | if(pendingPayment.isPayPalSupported()){ 31 | try{ 32 | String paypalCaptureId = null; 33 | if(pendingPayment.isRecurring()){ 34 | JsonObject obj = myPayPal.captureSubscription( 35 | pendingPayment.paypalSubscriptionId, 36 | new Converter().toPayPalCurrency(pendingPayment)); 37 | // TODO update db on success 38 | } 39 | else { 40 | Objects.requireNonNull(pendingPayment.paypalOrderId); 41 | if(paypalOrders.get(pendingPayment.paypalOrderId) == null) 42 | paypalOrders.put(pendingPayment.paypalOrderId, new ArrayList<>()); 43 | paypalOrders.get(pendingPayment.paypalOrderId) 44 | .add(pendingPayment); 45 | } 46 | } catch (Exception e) { 47 | e.printStackTrace(); 48 | } 49 | 50 | } 51 | } 52 | 53 | // CAPTURE PAYPAL ORDERS: 54 | for (List order : paypalOrders.values()) { 55 | long fullCharge = 0; 56 | for (Payment p: order) { 57 | fullCharge += p.charge; 58 | } 59 | Payment pFirst = order.get(0); 60 | try{ 61 | String captureId = myPayPal.captureOrder(paypalV2, pFirst.paypalOrderId, 62 | new Converter().toPayPalCurrency(pFirst.currency, fullCharge)); 63 | for (Payment p : order) { 64 | p.paypalCaptureId = captureId; 65 | p.timestampPaid = now; 66 | database.updatePayment(p); 67 | } 68 | } catch (Exception e) { 69 | e.printStackTrace(); 70 | } 71 | } 72 | 73 | msToSleep += msToSleepStart; 74 | threadCheckIfPaid.counter.incrementAndGet(); 75 | } 76 | } catch (Exception e) { 77 | throw new RuntimeException(e); 78 | } 79 | }; 80 | */ 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/paypal/PaypalWebhookEvent.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.paypal; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.JsonObject; 5 | import com.osiris.payhook.exceptions.ParseBodyException; 6 | import com.osiris.payhook.exceptions.ParseHeaderException; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | 12 | public class PaypalWebhookEvent { 13 | private final String validWebhookId; 14 | private final List validTypesList; 15 | private final String bodyString; 16 | private final PaypalWebhookEventHeader header; 17 | private final JsonObject body; 18 | private boolean isValid = false; 19 | 20 | public PaypalWebhookEvent(String validWebhookId, List validTypesList, Map header, String body) throws ParseHeaderException, ParseBodyException { 21 | this.validWebhookId = validWebhookId; 22 | this.validTypesList = validTypesList; 23 | this.header = new UtilsPayPal().parseAndGetHeader(header, validWebhookId); 24 | this.body = new UtilsPayPal().parseAndGetBody(body); 25 | this.bodyString = new Gson().toJson(body); 26 | } 27 | 28 | /** 29 | * The in-memory representation of a Webhook event/notification.
30 | * Can be validated through {@link PayPalUtils#isWebhookEventValid(PaypalWebhookEvent)}. 31 | * 32 | * @param validWebhookId your webhooks valid id. Get it from here: https://developer.paypal.com/developer/applications/ 33 | * @param validTypesList your webhooks valid types/names. Here is a full list: https://developer.paypal.com/docs/api-basics/notifications/webhooks/event-names/ 34 | * @param header the http messages header as {@link PaypalWebhookEventHeader}. 35 | * @param body the http messages body as {@link JsonObject}. 36 | */ 37 | public PaypalWebhookEvent(String validWebhookId, List validTypesList, PaypalWebhookEventHeader header, JsonObject body) { 38 | this.validWebhookId = validWebhookId; 39 | this.validTypesList = validTypesList; 40 | this.header = header; 41 | this.body = body; 42 | this.bodyString = new Gson().toJson(body); 43 | } 44 | 45 | public String getValidWebhookId() { 46 | return validWebhookId; 47 | } 48 | 49 | public List getValidTypesList() { 50 | return validTypesList; 51 | } 52 | 53 | public String getBodyString() { 54 | return bodyString; 55 | } 56 | 57 | public PaypalWebhookEventHeader getHeader() { 58 | return header; 59 | } 60 | 61 | public JsonObject getBody() { 62 | return body; 63 | } 64 | 65 | /** 66 | * Perform {@link PayPalUtils#isWebhookEventValid(PaypalWebhookEvent)} on this event, so this method 67 | * returns the right value. 68 | * 69 | * @return true if this event is a valid paypal webhook event. 70 | */ 71 | public boolean isValid() { 72 | return isValid; 73 | } 74 | 75 | public void setValid(boolean valid) { 76 | isValid = valid; 77 | } 78 | 79 | /** 80 | * Shortcut for returning the id from the json body. 81 | */ 82 | public String getId() { 83 | return body.get("id").getAsString(); 84 | } 85 | 86 | /** 87 | * Shortcut for returning the summary from the json body. 88 | */ 89 | public String getSummary() { 90 | return body.get("summary").getAsString(); 91 | } 92 | 93 | /** 94 | * Shortcut for returning the event_type from the json body. 95 | */ 96 | public String getEventType() { 97 | return body.get("event_type").getAsString(); 98 | } 99 | 100 | /** 101 | * Shortcut for returning the resource_type from the json body. 102 | */ 103 | public String getResourceType() { 104 | return body.get("resource_type").getAsString(); 105 | } 106 | 107 | /** 108 | * Shortcut for returning the event_version from the json body. 109 | */ 110 | public String getEventVersion() { 111 | return body.get("event_version").getAsString(); 112 | } 113 | 114 | /** 115 | * Shortcut for returning the event_version from the json body. 116 | */ 117 | public String getResourceVersion() { 118 | return body.get("resource_version").getAsString(); 119 | } 120 | 121 | } 122 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/paypal/UtilsPayPal.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Osiris Team 3 | * All rights reserved. 4 | * 5 | * This software is copyrighted work licensed under the terms of the 6 | * AutoPlug License. Please consult the file "LICENSE" for details. 7 | */ 8 | 9 | package com.osiris.payhook.paypal; 10 | 11 | import com.google.gson.JsonObject; 12 | import com.google.gson.JsonParser; 13 | import com.osiris.payhook.exceptions.ParseBodyException; 14 | import com.osiris.payhook.exceptions.ParseHeaderException; 15 | 16 | import java.util.Map; 17 | import java.util.Objects; 18 | 19 | public class UtilsPayPal { 20 | 21 | public PayPalPlan.Status getPlanStatus(String statusAsString) { 22 | if (statusAsString.equalsIgnoreCase(PayPalPlan.Status.ACTIVE.name())) 23 | return PayPalPlan.Status.ACTIVE; 24 | else if (statusAsString.equalsIgnoreCase(PayPalPlan.Status.INACTIVE.name())) 25 | return PayPalPlan.Status.INACTIVE; 26 | else if (statusAsString.equalsIgnoreCase(PayPalPlan.Status.CREATED.name())) 27 | return PayPalPlan.Status.CREATED; 28 | else 29 | return null; 30 | } 31 | 32 | public PayPalSubscription.Status getSubscriptionStatus(String statusAsString) { 33 | if (statusAsString.equalsIgnoreCase(PayPalSubscription.Status.APPROVAL_PENDING.name())) 34 | return PayPalSubscription.Status.APPROVAL_PENDING; 35 | else if (statusAsString.equalsIgnoreCase(PayPalSubscription.Status.APPROVED.name())) 36 | return PayPalSubscription.Status.APPROVED; 37 | else if (statusAsString.equalsIgnoreCase(PayPalSubscription.Status.ACTIVE.name())) 38 | return PayPalSubscription.Status.ACTIVE; 39 | else if (statusAsString.equalsIgnoreCase(PayPalSubscription.Status.SUSPENDED.name())) 40 | return PayPalSubscription.Status.SUSPENDED; 41 | else if (statusAsString.equalsIgnoreCase(PayPalSubscription.Status.CANCELLED.name())) 42 | return PayPalSubscription.Status.CANCELLED; 43 | else if (statusAsString.equalsIgnoreCase(PayPalSubscription.Status.EXPIRED.name())) 44 | return PayPalSubscription.Status.EXPIRED; 45 | else 46 | return null; 47 | } 48 | 49 | /** 50 | * Parses the provided header {@link Map} 51 | * into a {@link PaypalWebhookEventHeader} object and returns it. 52 | */ 53 | public PaypalWebhookEventHeader parseAndGetHeader(Map headerAsMap, String webhookId) throws ParseHeaderException { 54 | // Check if all keys we need exist 55 | String transmissionId = checkKeyAndGetValue(headerAsMap, Constants.PAYPAL_HEADER_TRANSMISSION_ID); 56 | String timestamp = checkKeyAndGetValue(headerAsMap, Constants.PAYPAL_HEADER_TRANSMISSION_TIME); 57 | String transmissionSignature = checkKeyAndGetValue(headerAsMap, Constants.PAYPAL_HEADER_TRANSMISSION_SIG); 58 | String certUrl = checkKeyAndGetValue(headerAsMap, Constants.PAYPAL_HEADER_CERT_URL); 59 | String authAlgorithm = checkKeyAndGetValue(headerAsMap, Constants.PAYPAL_HEADER_AUTH_ALGO); 60 | return new PaypalWebhookEventHeader(transmissionId, timestamp, transmissionSignature, authAlgorithm, certUrl, webhookId); 61 | } 62 | 63 | /** 64 | * Parses the provided body {@link String} 65 | * into a {@link JsonObject} and returns it. 66 | */ 67 | public JsonObject parseAndGetBody(String bodyAsString) throws ParseBodyException { 68 | try { 69 | return JsonParser.parseString(bodyAsString).getAsJsonObject(); 70 | } catch (Exception e) { 71 | throw new ParseBodyException(e.getMessage()); 72 | } 73 | } 74 | 75 | /** 76 | * Checks if the provided key exists in the provided map and returns its value.
77 | * The keys existence is checked by {@link String#equalsIgnoreCase(String)}, so that its case is ignored.
78 | * 79 | * @return the value mapped to the provided key. 80 | */ 81 | public String checkKeyAndGetValue(Map map, String key) throws ParseHeaderException { 82 | Objects.requireNonNull(map); 83 | Objects.requireNonNull(key); 84 | 85 | String value = map.get(key); 86 | if (value == null || value.equals("")) { 87 | for (Map.Entry entry : map.entrySet()) { 88 | if (entry.getKey().equalsIgnoreCase(key)) { 89 | value = entry.getValue(); 90 | break; 91 | } 92 | } 93 | 94 | if (value == null || value.equals("")) { 95 | throw new ParseHeaderException("Header is missing the '" + key + "' key or its value!"); 96 | } 97 | } 98 | return value; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/utils/Converter.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.utils; 2 | 3 | import com.osiris.jsqlgen.payhook.Product; 4 | import com.paypal.api.payments.Currency; 5 | import com.paypal.api.payments.MerchantPreferences; 6 | import com.paypal.api.payments.PaymentDefinition; 7 | import com.paypal.payments.Money; 8 | 9 | import java.math.BigDecimal; 10 | import java.util.ArrayList; 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | public class Converter { 16 | 17 | public long toSmallestCurrency(com.paypal.orders.Money amount) { 18 | return new BigDecimal(amount.value()).multiply(new BigDecimal(100)).longValue(); 19 | } 20 | 21 | public long toSmallestCurrency(Money amount) { 22 | return new BigDecimal(amount.value()).multiply(new BigDecimal(100)).longValue(); 23 | } 24 | 25 | public Money toPayPalMoney(String currency, long priceInSmallestCurrency) { 26 | return new Money().currencyCode(currency).value(new BigDecimal(priceInSmallestCurrency).divide(new BigDecimal(100)).toPlainString()); 27 | } 28 | 29 | public com.paypal.api.payments.Currency toPayPalCurrency(String currency, long priceInSmallestCurrency) { 30 | return new com.paypal.api.payments.Currency(currency, new BigDecimal(priceInSmallestCurrency).divide(new BigDecimal(100)).toPlainString()); 31 | } 32 | 33 | public com.paypal.api.payments.Currency toPayPalCurrency(Product product) { 34 | return new com.paypal.api.payments.Currency(product.currency, new BigDecimal(product.charge).divide(new BigDecimal(100)).toPlainString()); 35 | } 36 | 37 | public String toMoneyString(Product product) { 38 | Currency currency = toPayPalCurrency(product); 39 | return currency.getValue() + " " + currency.getCurrency(); 40 | } 41 | 42 | public String toMoneyString(String currency, long cents) { 43 | Currency currency1 = toPayPalCurrency(currency, cents); 44 | return currency1.getValue() + " " + currency1.getCurrency(); 45 | } 46 | 47 | public Map toStripeProduct(Product product) { 48 | Map params = new HashMap<>(); 49 | params.put("name", product.name); 50 | params.put("description", product.description); 51 | return params; 52 | } 53 | 54 | public Map toStripePrice(Product product) { 55 | Map paramsPrice = new HashMap<>(); 56 | paramsPrice.put("currency", product.currency); 57 | paramsPrice.put("product", product.stripeProductId); 58 | paramsPrice.put("unit_amount", product.charge); 59 | if (product.isRecurring()) { 60 | Map recurring = new HashMap<>(); 61 | recurring.put("interval", "day"); 62 | if (product.paymentInterval <= 0) 63 | throwInvalidPaymentInterval(product.paymentInterval); 64 | recurring.put("interval_count", (long) product.paymentInterval); 65 | recurring.put("usage_type", "licensed"); 66 | 67 | paramsPrice.put("recurring", recurring); 68 | } 69 | return paramsPrice; 70 | } 71 | 72 | public com.paypal.api.payments.Plan toPayPalPlan(Product product, String successUrl, String cancelUrl) { 73 | com.paypal.api.payments.Plan plan = new com.paypal.api.payments.Plan(product.name, product.description, "INFINITE"); 74 | plan.setMerchantPreferences(new MerchantPreferences() 75 | .setReturnUrl(successUrl) 76 | .setCancelUrl(cancelUrl) 77 | .setAutoBillAmount("YES")); 78 | List paymentDefinitions = new ArrayList<>(1); 79 | PaymentDefinition paymentDefinition = new PaymentDefinition() 80 | .setName("Payment for " + product.name) 81 | .setAmount(toPayPalCurrency(product)) 82 | .setType("REGULAR"); 83 | paymentDefinition.setFrequency("DAY"); 84 | if (product.paymentInterval <= 0) 85 | throwInvalidPaymentInterval(product.paymentInterval); 86 | paymentDefinition.setFrequencyInterval("" + product.paymentInterval); 87 | 88 | paymentDefinitions.add(paymentDefinition); 89 | plan.setPaymentDefinitions(paymentDefinitions); 90 | return plan; 91 | } 92 | 93 | private void throwInvalidPaymentInterval(int paymentInterval) { 94 | throw new IllegalArgumentException("Payment interval (" + paymentInterval + ") cannot be <= 0 if product is meant to have recurring payments!"); 95 | } 96 | 97 | public List toPayPalPlanPatch(Product product) { 98 | List patches = new ArrayList<>(); 99 | patches.add(new com.paypal.api.payments.Patch("replace", "name").setValue(product.name)); 100 | patches.add(new com.paypal.api.payments.Patch("replace", "description").setValue(product.description)); 101 | return patches; 102 | } 103 | 104 | 105 | } 106 | -------------------------------------------------------------------------------- /src/test/java/com/osiris/payhook/TestPayment.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook; 2 | 3 | import com.osiris.jsqlgen.payhook.Database; 4 | 5 | import java.sql.PreparedStatement; 6 | import java.sql.ResultSet; 7 | import java.sql.Statement; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | public class TestPayment { 12 | private static final java.sql.Connection con; 13 | private static final java.util.concurrent.atomic.AtomicInteger idCounter = new java.util.concurrent.atomic.AtomicInteger(0); 14 | 15 | static { 16 | try { 17 | con = java.sql.DriverManager.getConnection(Database.url, Database.username, Database.password); 18 | try (Statement s = con.createStatement()) { 19 | s.executeUpdate("CREATE TABLE IF NOT EXISTS `TestPayment` (id INT NOT NULL PRIMARY KEY)"); 20 | s.executeUpdate("ALTER TABLE `TestPayment` ADD COLUMN IF NOT EXISTS id INT NOT NULL PRIMARY KEY"); 21 | s.executeUpdate("ALTER TABLE `TestPayment` MODIFY IF EXISTS id INT NOT NULL PRIMARY KEY"); 22 | s.executeUpdate("ALTER TABLE `TestPayment` ADD COLUMN IF NOT EXISTS name VARCHAR(255)"); 23 | s.executeUpdate("ALTER TABLE `TestPayment` MODIFY IF EXISTS name VARCHAR(255)"); 24 | s.executeUpdate("ALTER TABLE `TestPayment` ADD COLUMN IF NOT EXISTS age BIGINT"); 25 | s.executeUpdate("ALTER TABLE `TestPayment` MODIFY IF EXISTS age BIGINT"); 26 | } 27 | try (PreparedStatement ps = con.prepareStatement("SELECT id FROM `TestPayment` ORDER BY id DESC LIMIT 1")) { 28 | ResultSet rs = ps.executeQuery(); 29 | if (rs.next()) idCounter.set(rs.getInt(1)); 30 | } 31 | } catch (Exception e) { 32 | throw new RuntimeException(e); 33 | } 34 | } 35 | 36 | /** 37 | * Database field/value.
38 | */ 39 | public int id; 40 | /** 41 | * Database field/value.
42 | */ 43 | public String name; 44 | /** 45 | * Database field/value.
46 | */ 47 | public long age; 48 | public TestPayment() { 49 | } 50 | 51 | /** 52 | * Increments the id and sets it for this object (basically reserves a space in the database). 53 | * 54 | * @return object with latest id. Should be added to the database next by you. 55 | */ 56 | public static TestPayment create() { 57 | TestPayment obj = new TestPayment(); 58 | obj.id = idCounter.incrementAndGet(); 59 | return obj; 60 | } 61 | 62 | public static List get() throws Exception { 63 | return get(null); 64 | } 65 | 66 | /** 67 | * @return a list containing only objects that match the provided SQL WHERE statement. 68 | * if that statement is null, returns all the contents of this table. 69 | */ 70 | public static List get(String where) throws Exception { 71 | List list = new ArrayList<>(); 72 | try (PreparedStatement ps = con.prepareStatement( 73 | "SELECT id,name,age" + 74 | " FROM `TestPayment`" + 75 | (where != null ? ("WHERE " + where) : ""))) { 76 | ResultSet rs = ps.executeQuery(); 77 | while (rs.next()) { 78 | TestPayment obj = new TestPayment(); 79 | list.add(obj); 80 | obj.id = rs.getInt(1); 81 | obj.name = rs.getString(2); 82 | obj.age = rs.getLong(3); 83 | } 84 | } 85 | return list; 86 | } 87 | 88 | /** 89 | * Searches the provided object in the database (by its id), 90 | * and updates all its fields. 91 | * 92 | * @throws Exception when failed to find by id. 93 | */ 94 | public static void update(TestPayment obj) throws Exception { 95 | try (PreparedStatement ps = con.prepareStatement( 96 | "UPDATE `TestPayment` SET id=?,name=?,age=?")) { 97 | ps.setInt(1, obj.id); 98 | ps.setString(2, obj.name); 99 | ps.setLong(3, obj.age); 100 | ps.executeUpdate(); 101 | } 102 | } 103 | 104 | /** 105 | * Adds the provided object to the database (note that the id is not checked for duplicates). 106 | */ 107 | public static void add(TestPayment obj) throws Exception { 108 | try (PreparedStatement ps = con.prepareStatement( 109 | "INSERT INTO `TestPayment` (id,name,age) VALUES (?,?,?)")) { 110 | ps.setInt(1, obj.id); 111 | ps.setString(2, obj.name); 112 | ps.setLong(3, obj.age); 113 | ps.executeUpdate(); 114 | } 115 | } 116 | 117 | /** 118 | * Deletes the provided object from the database. 119 | */ 120 | public static void delete(TestPayment obj) throws Exception { 121 | delete("id = " + obj.id); 122 | } 123 | 124 | /** 125 | * Deletes the objects that are found by the provided SQL WHERE statement, from the database. 126 | */ 127 | public static void delete(String where) throws Exception { 128 | java.util.Objects.requireNonNull(where); 129 | try (PreparedStatement ps = con.prepareStatement( 130 | "DELETE FROM `TestPayment` WHERE " + where)) { 131 | ps.executeUpdate(); 132 | } 133 | } 134 | 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/jsqlgen/payhook/Database.java: -------------------------------------------------------------------------------- 1 | package com.osiris.jsqlgen.payhook; 2 | 3 | import java.sql.Connection; 4 | import java.sql.DriverManager; 5 | import java.sql.SQLException; 6 | import java.sql.Statement; 7 | import java.util.ArrayList; 8 | import java.util.Arrays; 9 | import java.util.List; 10 | import java.util.Objects; 11 | 12 | /* 13 | Auto-generated class that is used by all table classes to create connections.
14 | It holds the database credentials (set by you at first run of jSQL-Gen).
15 | Note that the fields rawUrl, url, username and password do NOT get overwritten when re-generating this class.
16 | All tables use the cached connection pool in this class which has following advantages:
17 | - Ensures optimal performance (cpu and memory usage) for any type of database from small to huge, with millions of queries per second. 18 | - Connection status is checked before doing a query (since it could be closed or timed out and thus result in errors).*/ 19 | public class Database{ 20 | public static String url = com.osiris.payhook.PayHook.databaseUrl; 21 | public static String rawUrl = com.osiris.payhook.PayHook.databaseRawUrl; 22 | public static String name = com.osiris.payhook.PayHook.databaseName; 23 | public static String username = com.osiris.payhook.PayHook.databaseUsername; 24 | public static String password = com.osiris.payhook.PayHook.databasePassword; 25 | /** 26 | * Use synchronized on this before doing changes to it. 27 | */ 28 | public static final List availableConnections = new ArrayList<>(); 29 | 30 | static{create();} // Create database if not exists 31 | 32 | public static void create() { 33 | 34 | // Do the below to avoid "No suitable driver found..." exception 35 | String[] driversClassNames = new String[]{"com.mysql.cj.jdbc.Driver", "com.mysql.jdbc.Driver", 36 | "oracle.jdbc.OracleDriver", "com.microsoft.sqlserver.jdbc.SQLServerDriver", "org.postgresql.Driver", 37 | "org.sqlite.JDBC", "org.h2.Driver", "com.ibm.db2.jcc.DB2Driver", "org.apache.derby.jdbc.ClientDriver", 38 | "org.mariadb.jdbc.Driver", "org.apache.derby.jdbc.ClientDriver"}; 39 | Class driverClass = null; 40 | Exception lastException = null; 41 | for (int i = 0; i < driversClassNames.length; i++) { 42 | String driverClassName = driversClassNames[i]; 43 | try { 44 | driverClass = Class.forName(driverClassName); 45 | Objects.requireNonNull(driverClass); 46 | break; // No need to continue, since registration was a success 47 | } catch (Exception e) { 48 | lastException = e; 49 | } 50 | } 51 | if(driverClass == null){ 52 | if(lastException != null) lastException.printStackTrace(); 53 | System.err.println("Failed to find critical database driver class, program will exit! Searched classes: "+ Arrays.toString(driversClassNames)); 54 | System.exit(1); 55 | } 56 | 57 | // Create database if not exists 58 | try(Connection c = DriverManager.getConnection(Database.rawUrl, Database.username, Database.password); 59 | Statement s = c.createStatement();) { 60 | s.executeUpdate("CREATE DATABASE IF NOT EXISTS `"+Database.name+"`"); 61 | } catch (SQLException e) { 62 | e.printStackTrace(); 63 | System.err.println("Something went really wrong during database initialisation, program will exit."); 64 | System.exit(1); 65 | } 66 | } 67 | 68 | public static Connection getCon() { 69 | synchronized (availableConnections){ 70 | try{ 71 | Connection availableCon = null; 72 | if (!availableConnections.isEmpty()) { 73 | List removableConnections = new ArrayList<>(0); 74 | for (Connection con : availableConnections) { 75 | if (!con.isValid(1)) {con.close(); removableConnections.add(con);} 76 | else {availableCon = con; removableConnections.add(con); break;} 77 | } 78 | for (Connection removableConnection : removableConnections) { 79 | availableConnections.remove(removableConnection); // Remove invalid or used connections 80 | } 81 | } 82 | if (availableCon != null) return availableCon; 83 | else return DriverManager.getConnection(Database.url, Database.username, Database.password); 84 | } catch (Exception e) { 85 | throw new RuntimeException(e); 86 | } 87 | } 88 | } 89 | 90 | public static void freeCon(Connection connection) { 91 | synchronized (availableConnections){ 92 | availableConnections.add(connection); 93 | } 94 | } 95 | /** 96 | * Gets the raw database url without database name.
97 | * Before: "jdbc:mysql://localhost/my_database"
98 | * After: "jdbc:mysql://localhost"
99 | */ 100 | public static String getRawDbUrlFrom(String databaseUrl) { 101 | int index = 0; 102 | int count = 0; 103 | for (int i = 0; i < databaseUrl.length(); i++) { 104 | char c = databaseUrl.charAt(i); 105 | if(c == '/'){ 106 | index = i; 107 | count++; 108 | } 109 | if(count == 3) break; 110 | } 111 | if(count != 3) return databaseUrl; // Means there is less than 3 "/", thus may already be raw url, or totally wrong url 112 | return databaseUrl.substring(0, index); 113 | }} 114 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/paypal/UtilsPayPalJson.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Osiris Team 3 | * All rights reserved. 4 | * 5 | * This software is copyrighted work licensed under the terms of the 6 | * AutoPlug License. Please consult the file "LICENSE" for details. 7 | */ 8 | 9 | package com.osiris.payhook.paypal; 10 | 11 | import com.google.gson.*; 12 | import com.osiris.jlib.json.exceptions.HttpErrorException; 13 | import com.osiris.jlib.json.exceptions.WrongJsonTypeException; 14 | import okhttp3.*; 15 | 16 | import java.io.IOException; 17 | import java.util.ArrayList; 18 | import java.util.Arrays; 19 | import java.util.List; 20 | 21 | 22 | public class UtilsPayPalJson { 23 | // Use OkHttp because I couldn't find a way of 24 | // sending PATCH http requests to PayPal 25 | // without Java throwing exceptions... 26 | public static MediaType JSON = MediaType.get("application/json; charset=utf-8"); 27 | public static OkHttpClient CLIENT = new OkHttpClient(); 28 | 29 | public JsonElement jsonFromUrl(String requestMethod, String url, JsonElement elementToSend, PayPalUtils payPalUtils, Integer... successCodes) throws IOException, HttpErrorException { 30 | Request.Builder requestBuilder = new Request.Builder() 31 | .url(url) 32 | .header("User-Agent", "PayHook") 33 | .header("Content-Type", "application/json") 34 | .header("Authorization", "Basic " + payPalUtils.getCredBase64()) 35 | .header("return", "representation"); 36 | if (elementToSend != null) { 37 | RequestBody body = RequestBody.create(new Gson().toJson(elementToSend), JSON); 38 | requestBuilder.method(requestMethod, body); 39 | } else { 40 | requestBuilder.method(requestMethod, null); 41 | } 42 | try (Response response = CLIENT.newCall(requestBuilder.build()).execute()) { 43 | int code = response.code(); 44 | ResponseBody body = response.body(); 45 | if (code == 200 || (successCodes != null && Arrays.asList(successCodes).contains(code))) { 46 | if (body == null) return null; 47 | else return JsonParser.parseString(body.string()); 48 | } else { 49 | throw new HttpErrorException(code, null, "\nurl: " + url + "\ncode: "+code+" \nmessage: " + response.message() + "\njson: \n" 50 | + (body != null ? body.string() : "")); 51 | } 52 | } 53 | } 54 | 55 | public JsonElement postJsonAndGetResponse(String url, JsonElement element, PayPalUtils context) throws IOException, HttpErrorException { 56 | return jsonFromUrl("POST", url, element, context, (Integer[]) null); 57 | } 58 | 59 | public JsonElement postJsonAndGetResponse(String url, JsonElement element, PayPalUtils context, Integer... successCodes) throws IOException, HttpErrorException { 60 | return jsonFromUrl("POST", url, element, context, successCodes); 61 | } 62 | 63 | public JsonElement patchJsonAndGetResponse(String url, JsonElement element, PayPalUtils context) throws IOException, HttpErrorException { 64 | return jsonFromUrl("PATCH", url, element, context, (Integer[]) null); 65 | } 66 | 67 | public JsonElement patchJsonAndGetResponse(String url, JsonElement element, PayPalUtils context, Integer... successCodes) throws IOException, HttpErrorException { 68 | return jsonFromUrl("PATCH", url, element, context, successCodes); 69 | } 70 | 71 | public JsonElement deleteAndGetResponse(String url, PayPalUtils context) throws IOException, HttpErrorException { 72 | return jsonFromUrl("DELETE", url, null, context, 204); 73 | } 74 | 75 | public JsonElement deleteAndGetResponse(String url, PayPalUtils context, Integer... successCodes) throws IOException, HttpErrorException { 76 | return jsonFromUrl("DELETE", url, null, context, successCodes); 77 | } 78 | 79 | /** 80 | * Returns the json-element. This can be a json-array or a json-object. 81 | * 82 | * @param input_url The url which leads to the json file. 83 | * @return JsonElement 84 | * @throws Exception When status code other than 200. 85 | */ 86 | public JsonElement getJsonElement(String input_url, PayPalUtils context) throws IOException, HttpErrorException { 87 | return jsonFromUrl("GET", input_url, null, context, (Integer[]) null); 88 | } 89 | 90 | public JsonArray getJsonArray(String url, PayPalUtils context) throws IOException, HttpErrorException, WrongJsonTypeException { 91 | JsonElement element = getJsonElement(url, context); 92 | if (element != null && element.isJsonArray()) { 93 | return element.getAsJsonArray(); 94 | } else { 95 | throw new WrongJsonTypeException("Its not a json array! Check it out -> " + url); 96 | } 97 | } 98 | 99 | /** 100 | * Turns a JsonArray with its objects into a list. 101 | * 102 | * @param url The url where to find the json file. 103 | * @return A list with JsonObjects or null if there was a error with the url. 104 | */ 105 | public List getJsonArrayAsList(String url, PayPalUtils context) throws IOException, HttpErrorException, WrongJsonTypeException { 106 | List objectList = new ArrayList<>(); 107 | JsonElement element = getJsonElement(url, context); 108 | if (element != null && element.isJsonArray()) { 109 | final JsonArray ja = element.getAsJsonArray(); 110 | for (int i = 0; i < ja.size(); i++) { 111 | JsonObject jo = ja.get(i).getAsJsonObject(); 112 | objectList.add(jo); 113 | } 114 | return objectList; 115 | } else { 116 | throw new WrongJsonTypeException("Its not a json array! Check it out -> " + url); 117 | } 118 | } 119 | 120 | /** 121 | * Gets a single JsonObject. 122 | * 123 | * @param url The url where to find the json file. 124 | * @return A JsonObject or null if there was a error with the url. 125 | */ 126 | public JsonObject getJsonObject(String url, PayPalUtils context) throws IOException, HttpErrorException, WrongJsonTypeException { 127 | JsonElement element = getJsonElement(url, context); 128 | if (element != null && element.isJsonObject()) { 129 | return element.getAsJsonObject(); 130 | } else { 131 | throw new WrongJsonTypeException("Its not a json object! Check it out -> " + url); 132 | } 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/Subscription.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook; 2 | 3 | import com.osiris.jsqlgen.payhook.Payment; 4 | import com.osiris.payhook.utils.Converter; 5 | import com.osiris.payhook.utils.UtilsTime; 6 | 7 | import java.util.*; 8 | 9 | public class Subscription { 10 | public List payments; 11 | 12 | /** 13 | * @param payments must all have the same subscription id. 14 | */ 15 | public Subscription(Payment... payments) { 16 | this.payments = new ArrayList<>(Arrays.asList(payments)); // Copy since asList is unmodifiable 17 | } 18 | 19 | /** 20 | * @param payments must all have the same subscription id. 21 | */ 22 | public Subscription(List payments) { 23 | this.payments = payments; 24 | } 25 | 26 | public static List getForUser(String userId) { 27 | //TODO ADD PAYMENT PROCESSOR 28 | List payments = Payment.get("WHERE userId=? AND (paypalSubscriptionId IS NOT NULL OR stripeSubscriptionId IS NOT NULL)" 29 | , userId); 30 | return paymentsToSubscriptions(payments); 31 | } 32 | 33 | public static List get() { 34 | //TODO ADD PAYMENT PROCESSOR 35 | List payments = Payment.get("WHERE paypalSubscriptionId IS NOT NULL OR stripeSubscriptionId IS NOT NULL"); 36 | return paymentsToSubscriptions(payments); 37 | } 38 | 39 | public static List getNotCancelled() { 40 | //TODO ADD PAYMENT PROCESSOR 41 | List payments = Payment.get("WHERE (paypalSubscriptionId IS NOT NULL OR stripeSubscriptionId IS NOT NULL)" + 42 | " AND timestampCancelled = 0"); 43 | return paymentsToSubscriptions(payments); 44 | } 45 | 46 | public static List paymentsToSubscriptions(List payments) { 47 | //TODO ADD PAYMENT PROCESSOR 48 | Map> paypalSubs = new HashMap<>(); 49 | Map> stripeSubs = new HashMap<>(); 50 | for (Payment payment : payments) { 51 | if (payment.paypalSubscriptionId != null) { 52 | List list = paypalSubs.computeIfAbsent(payment.paypalSubscriptionId, k -> new ArrayList<>()); 53 | list.add(payment); 54 | } 55 | } 56 | for (Payment payment : payments) { 57 | if (payment.stripeSubscriptionId != null) { 58 | List list = stripeSubs.computeIfAbsent(payment.stripeSubscriptionId, k -> new ArrayList<>()); 59 | list.add(payment); 60 | } 61 | } 62 | 63 | List subs = new ArrayList<>(); 64 | paypalSubs.forEach((id, listPayments) -> { 65 | subs.add(new Subscription(listPayments)); 66 | }); 67 | stripeSubs.forEach((id, listPayments) -> { 68 | subs.add(new Subscription(listPayments)); 69 | }); 70 | return subs; 71 | } 72 | 73 | public Subscription refund() { 74 | 75 | return this; 76 | } 77 | 78 | public Subscription cancel() throws Exception { 79 | Payment lastPayment = getLastPayment(); 80 | PayHook.cancelPayment(lastPayment); 81 | return this; 82 | } 83 | 84 | public Payment getLastPayment() { 85 | return payments.get(payments.size() - 1); 86 | } 87 | 88 | /** 89 | * Calculates the amount of milliseconds this subscription is still valid for.
90 | * If the customer doesn't pay in time, this method returns a negative value.
91 | * Ignores {@link Payment#timestampCancelled} and relies on {@link Payment#timestampAuthorized} 92 | * of the last {@link Payment}. 93 | */ 94 | public long getMillisLeft() { 95 | Payment lastAuthorizedPayment = getLastAuthorizedPayment(); 96 | if (lastAuthorizedPayment == null) return 0; 97 | long totalMillisValid = Payment.Interval.toMilliseconds(lastAuthorizedPayment.interval); 98 | return totalMillisValid - (System.currentTimeMillis() - lastAuthorizedPayment.timestampAuthorized); 99 | } 100 | 101 | /** 102 | * Same as {@link #getMillisLeft()} but adds the payment processor 103 | * specific puffer: {@link PayHook#paypalUrlTimeoutMs} or 104 | * {@link PayHook#stripeUrlTimeoutMs}. 105 | */ 106 | public long getMillisLeftWithPuffer() { 107 | Payment lastAuthorizedPayment = getLastAuthorizedPayment(); 108 | if (lastAuthorizedPayment == null) return 0; 109 | if (lastAuthorizedPayment.stripeSubscriptionId != null) 110 | return getMillisLeft() + PayHook.stripeUrlTimeoutMs; 111 | else if (lastAuthorizedPayment.paypalSubscriptionId != null) 112 | return getMillisLeft() + PayHook.paypalUrlTimeoutMs; 113 | else 114 | throw new IllegalStateException("Last payment of subscription must have a payment-processor specific subscription id!"); 115 | //TODO ADD PAYMENT PROCESSOR 116 | } 117 | 118 | public boolean isCancelled() { 119 | return getLastPayment().timestampCancelled != 0; 120 | } 121 | 122 | /** 123 | * True if {@link #getMillisLeftWithPuffer()} is > 0.
124 | * Does not check if the subscription was cancelled. Only checks the last payment 125 | * and returns true if there is still time left. 126 | */ 127 | public boolean isTimeLeft() { 128 | return getMillisLeftWithPuffer() > 0; 129 | } 130 | 131 | public Payment getLastAuthorizedPayment() { 132 | for (int i = payments.size() - 1; i > -1; i--) { 133 | Payment payment = payments.get(i); 134 | if (payment.isAuthorized()) { 135 | return payment; 136 | } 137 | } 138 | return null; 139 | } 140 | 141 | public long getTotalPaid() { 142 | long cents = 0; 143 | for (Payment payment : payments) { 144 | cents += payment.charge; 145 | } 146 | return cents; 147 | } 148 | 149 | public String toPrintString() { 150 | Payment lastPayment = getLastPayment(); 151 | long millisLeft = getMillisLeft(); 152 | long millisLeftWithPuffer = getMillisLeftWithPuffer(); 153 | return "userid=" + lastPayment.userId + " payments=" + payments.size() + "" + 154 | " time-left=" + (millisLeft > 0 ? new UtilsTime().getFormattedString(millisLeft) : "") 155 | + " time-left-with-puffer=" + (millisLeftWithPuffer > 0 ? new UtilsTime().getFormattedString(millisLeftWithPuffer) : "") 156 | + " last-authorized-payment=" + (lastPayment.timestampAuthorized > 0 ? new Date(lastPayment.timestampAuthorized) : "") 157 | + " total-paid=" + new Converter().toMoneyString(lastPayment.currency, getTotalPaid()); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.osiris.payhook 8 | PayHook 9 | LATEST 10 | jar 11 | 12 | PayHook 13 | Make payments via Java easy. 14 | 15 | 16 | 8 17 | 8 18 | 8 19 | UTF-8 20 | 21 | 22 | 23 | 24 | jitpack.io 25 | https://jitpack.io 26 | 27 | 28 | 29 | 30 | 31 | com.squareup.okhttp3 32 | okhttp 33 | 5.0.0-alpha.2 34 | 35 | 36 | com.github.Osiris-Team 37 | jlib 38 | 16.10 39 | 40 | 41 | com.github.Osiris-Team 42 | Easy-Java-Events 43 | 1.8.0 44 | 45 | 46 | com.google.code.gson 47 | gson 48 | 2.8.6 49 | 50 | 51 | com.paypal.sdk 52 | checkout-sdk 53 | 1.0.5 54 | 55 | 56 | com.paypal.sdk 57 | rest-api-sdk 58 | 1.14.0 59 | 60 | 61 | com.braintreepayments.gateway 62 | braintree-java 63 | 3.14.0 64 | 65 | 66 | com.stripe 67 | stripe-java 68 | 20.87.0 69 | 70 | 71 | org.jetbrains 72 | annotations 73 | 22.0.0 74 | 75 | 76 | 77 | io.muserver 78 | mu-server 79 | 0.72.15 80 | test 81 | 82 | 83 | com.github.Osiris-Team 84 | Dyml 85 | 9.4.2 86 | test 87 | 88 | 89 | ch.vorburger.mariaDB4j 90 | mariaDB4j 91 | 2.4.0 92 | test 93 | 94 | 95 | org.junit.jupiter 96 | junit-jupiter 97 | RELEASE 98 | test 99 | 100 | 101 | com.github.Osiris-Team 102 | java-ngrok 103 | 1.6.1 104 | test 105 | 106 | 107 | mysql 108 | mysql-connector-java 109 | 8.0.24 110 | test 111 | 112 | 113 | com.github.Osiris-Team 114 | HBrowser 115 | 2.4.1 116 | test 117 | 118 | 119 | joda-time 120 | joda-time 121 | 2.10.10 122 | 123 | 124 | 125 | 126 | ${project.name} 127 | clean package 128 | 129 | 130 | 131 | org.apache.maven.plugins 132 | maven-compiler-plugin 133 | 3.8.1 134 | 135 | ${java.version} 136 | ${java.version} 137 | 138 | 139 | 140 | 141 | org.apache.maven.plugins 142 | maven-source-plugin 143 | 3.2.1 144 | 145 | 146 | attach-sources 147 | 148 | jar 149 | 150 | 151 | 152 | 153 | 154 | 155 | org.apache.maven.plugins 156 | maven-javadoc-plugin 157 | 3.0.0 158 | 159 | 160 | attach-javadocs 161 | 162 | jar 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | src/main/resources 173 | 174 | false 175 | 176 | 177 | 178 | 179 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/jsqlgen/payhook_structure.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "payhook", 3 | "tables": [ 4 | { 5 | "name": "Product", 6 | "columns": [ 7 | { 8 | "name": "id", 9 | "nameQuoted": "`id`", 10 | "definition": "INT NOT NULL PRIMARY KEY" 11 | }, 12 | { 13 | "name": "charge", 14 | "nameQuoted": "`charge`", 15 | "definition": "BIGINT NOT NULL" 16 | }, 17 | { 18 | "name": "currency", 19 | "nameQuoted": "`currency`", 20 | "definition": "CHAR(3) NOT NULL" 21 | }, 22 | { 23 | "name": "name", 24 | "nameQuoted": "`name`", 25 | "definition": "TEXT(65532) NOT NULL" 26 | }, 27 | { 28 | "name": "description", 29 | "nameQuoted": "`description`", 30 | "definition": "TEXT(65532) NOT NULL" 31 | }, 32 | { 33 | "name": "paymentInterval", 34 | "nameQuoted": "`paymentInterval`", 35 | "definition": "INT NOT NULL" 36 | }, 37 | { 38 | "name": "paypalProductId", 39 | "nameQuoted": "`paypalProductId`", 40 | "definition": "TEXT(65532) DEFAULT NULL" 41 | }, 42 | { 43 | "name": "paypalPlanId", 44 | "nameQuoted": "`paypalPlanId`", 45 | "definition": "TEXT(65532) DEFAULT NULL" 46 | }, 47 | { 48 | "name": "stripeProductId", 49 | "nameQuoted": "`stripeProductId`", 50 | "definition": "TEXT(65532) DEFAULT NULL" 51 | }, 52 | { 53 | "name": "stripePriceId", 54 | "nameQuoted": "`stripePriceId`", 55 | "definition": "TEXT(65532) DEFAULT NULL" 56 | } 57 | ], 58 | "isDebug": false, 59 | "isNoExceptions": true, 60 | "isCache": false 61 | }, 62 | { 63 | "name": "Payment", 64 | "columns": [ 65 | { 66 | "name": "id", 67 | "nameQuoted": "`id`", 68 | "definition": "INT NOT NULL PRIMARY KEY" 69 | }, 70 | { 71 | "name": "userId", 72 | "nameQuoted": "`userId`", 73 | "definition": "TEXT(65532) NOT NULL" 74 | }, 75 | { 76 | "name": "charge", 77 | "nameQuoted": "`charge`", 78 | "definition": "BIGINT NOT NULL", 79 | "comment": "The total charged amount in the smallest form of money. Example: 100 cents \u003d\u003d 1EUR.If not authorized yet, the money was not yet received.When refunded this normally is 0, or something greater on a partial refund.Note that cancelled does not mean refunded." 80 | }, 81 | { 82 | "name": "currency", 83 | "nameQuoted": "`currency`", 84 | "definition": "CHAR(3) NOT NULL" 85 | }, 86 | { 87 | "name": "interval", 88 | "nameQuoted": "`interval`", 89 | "definition": "INT NOT NULL" 90 | }, 91 | { 92 | "name": "url", 93 | "nameQuoted": "`url`", 94 | "definition": "TEXT(65532) DEFAULT NULL" 95 | }, 96 | { 97 | "name": "productId", 98 | "nameQuoted": "`productId`", 99 | "definition": "INT DEFAULT NULL" 100 | }, 101 | { 102 | "name": "productName", 103 | "nameQuoted": "`productName`", 104 | "definition": "TEXT(65532) DEFAULT NULL" 105 | }, 106 | { 107 | "name": "productQuantity", 108 | "nameQuoted": "`productQuantity`", 109 | "definition": "INT DEFAULT NULL" 110 | }, 111 | { 112 | "name": "timestampCreated", 113 | "nameQuoted": "`timestampCreated`", 114 | "definition": "BIGINT DEFAULT NULL" 115 | }, 116 | { 117 | "name": "timestampExpires", 118 | "nameQuoted": "`timestampExpires`", 119 | "definition": "BIGINT DEFAULT NULL" 120 | }, 121 | { 122 | "name": "timestampAuthorized", 123 | "nameQuoted": "`timestampAuthorized`", 124 | "definition": "BIGINT DEFAULT NULL" 125 | }, 126 | { 127 | "name": "timestampCancelled", 128 | "nameQuoted": "`timestampCancelled`", 129 | "definition": "BIGINT DEFAULT NULL" 130 | }, 131 | { 132 | "name": "timestampRefunded", 133 | "nameQuoted": "`timestampRefunded`", 134 | "definition": "BIGINT DEFAULT NULL" 135 | }, 136 | { 137 | "name": "stripeSessionId", 138 | "nameQuoted": "`stripeSessionId`", 139 | "definition": "TEXT(65532) DEFAULT NULL" 140 | }, 141 | { 142 | "name": "stripeSubscriptionId", 143 | "nameQuoted": "`stripeSubscriptionId`", 144 | "definition": "TEXT(65532) DEFAULT NULL" 145 | }, 146 | { 147 | "name": "stripePaymentIntentId", 148 | "nameQuoted": "`stripePaymentIntentId`", 149 | "definition": "TEXT(65532) DEFAULT NULL" 150 | }, 151 | { 152 | "name": "paypalOrderId", 153 | "nameQuoted": "`paypalOrderId`", 154 | "definition": "TEXT(65532) DEFAULT NULL" 155 | }, 156 | { 157 | "name": "paypalSubscriptionId", 158 | "nameQuoted": "`paypalSubscriptionId`", 159 | "definition": "TEXT(65532) DEFAULT NULL" 160 | }, 161 | { 162 | "name": "paypalCaptureId", 163 | "nameQuoted": "`paypalCaptureId`", 164 | "definition": "TEXT(65532) DEFAULT NULL" 165 | } 166 | ], 167 | "isDebug": false, 168 | "isNoExceptions": true, 169 | "isCache": false 170 | }, 171 | { 172 | "name": "PaymentWarning", 173 | "columns": [ 174 | { 175 | "name": "id", 176 | "nameQuoted": "`id`", 177 | "definition": "INT NOT NULL PRIMARY KEY" 178 | }, 179 | { 180 | "name": "paymentId", 181 | "nameQuoted": "`paymentId`", 182 | "definition": "INT NOT NULL" 183 | }, 184 | { 185 | "name": "message", 186 | "nameQuoted": "`message`", 187 | "definition": "TEXT(65532) DEFAULT NULL" 188 | } 189 | ], 190 | "isDebug": false, 191 | "isNoExceptions": true, 192 | "isCache": false 193 | }, 194 | { 195 | "name": "PendingPaymentCancel", 196 | "columns": [ 197 | { 198 | "name": "id", 199 | "nameQuoted": "`id`", 200 | "definition": "INT NOT NULL PRIMARY KEY" 201 | }, 202 | { 203 | "name": "paymentId", 204 | "nameQuoted": "`paymentId`", 205 | "definition": "INT NOT NULL" 206 | }, 207 | { 208 | "name": "timestampCancel", 209 | "nameQuoted": "`timestampCancel`", 210 | "definition": "BIGINT NOT NULL" 211 | } 212 | ], 213 | "isDebug": false, 214 | "isNoExceptions": true, 215 | "isCache": false 216 | }, 217 | { 218 | "name": "WebhookEndpoint", 219 | "columns": [ 220 | { 221 | "name": "id", 222 | "nameQuoted": "`id`", 223 | "definition": "INT NOT NULL PRIMARY KEY" 224 | }, 225 | { 226 | "name": "url", 227 | "nameQuoted": "`url`", 228 | "definition": "TEXT NOT NULL" 229 | }, 230 | { 231 | "name": "stripeWebhookSecret", 232 | "nameQuoted": "`stripeWebhookSecret`", 233 | "definition": "TEXT NOT NULL" 234 | } 235 | ], 236 | "isDebug": false, 237 | "isNoExceptions": true, 238 | "isCache": false 239 | } 240 | ], 241 | "javaProjectDir": "D:\\Coding\\JAVA\\PayHook" 242 | } -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/stripe/UtilsStripe.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook.stripe; 2 | 3 | import com.google.gson.JsonSyntaxException; 4 | import com.osiris.jsqlgen.payhook.Payment; 5 | import com.osiris.jsqlgen.payhook.Product; 6 | import com.osiris.payhook.PayHook; 7 | import com.osiris.payhook.PaymentProcessor; 8 | import com.osiris.payhook.Subscription; 9 | import com.osiris.payhook.exceptions.WebHookValidationException; 10 | import com.stripe.exception.SignatureVerificationException; 11 | import com.stripe.model.*; 12 | import com.stripe.model.checkout.Session; 13 | import com.stripe.net.Webhook; 14 | 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | public class UtilsStripe { 19 | 20 | /** 21 | * Checks if the provided webhook event is actually a real/valid one. 22 | * 23 | * @return null, if not valid. 24 | */ 25 | public Event checkWebhookEvent(String body, Map headers, String endpointSecret) 26 | throws JsonSyntaxException, SignatureVerificationException { // Invalid payload // Invalid signature 27 | String sigHeader = headers.get("Stripe-Signature"); 28 | if (sigHeader == null) { 29 | sigHeader = headers.get("stripe-signature"); // try lowercase 30 | if (sigHeader == null) 31 | throw new SignatureVerificationException("No Stripe-Signature/stripe-signature header present!", "---"); 32 | } 33 | return Webhook.constructEvent(body, sigHeader, endpointSecret); 34 | } 35 | 36 | public void handleEvent(Map header, String body, String stripeWebhookSecret) throws Exception { 37 | long now = System.currentTimeMillis(); 38 | Event event = checkWebhookEvent(body, header, stripeWebhookSecret); 39 | if (event == null) 40 | throw new WebHookValidationException("Received invalid webhook event (" + PaymentProcessor.STRIPE + ", validation failed)."); 41 | // Deserialize the nested object inside the event 42 | EventDataObjectDeserializer dataObjectDeserializer = event.getDataObjectDeserializer(); 43 | StripeObject stripeObject = null; 44 | if (dataObjectDeserializer.getObject().isPresent()) { 45 | stripeObject = dataObjectDeserializer.getObject().get(); 46 | } else { 47 | // Deserialization failed, probably due to an API version mismatch. 48 | // Refer to the Javadoc documentation on `EventDataObjectDeserializer` for 49 | // instructions on how to handle this case, or return an error here. 50 | throw new WebHookValidationException("Received invalid webhook event (" + PaymentProcessor.STRIPE + ", deserialization failed)."); 51 | } 52 | 53 | // Handle the event 54 | String type = event.getType(); 55 | if ("checkout.session.completed".equals(type)) {// Checkout payment was authorized/completed 56 | Session session = (Session) stripeObject; 57 | 58 | List paymentsWithSessionId = Payment.whereStripeSessionId().is(session.getId()).get(); 59 | if (paymentsWithSessionId.isEmpty()) 60 | throw new WebHookValidationException("Received invalid webhook event (" + PaymentProcessor.STRIPE + ", failed to find session id '" + session.getId() + "' in local database)."); 61 | long totalCharge = 0; 62 | for (Payment p : paymentsWithSessionId) { 63 | totalCharge += p.charge; 64 | } 65 | if (totalCharge != session.getAmountTotal()) 66 | throw new WebHookValidationException("Received invalid webhook event (" + PaymentProcessor.STRIPE + ", expected paid amount of '" + totalCharge + "' but got '" + session.getAmountTotal() + "')."); 67 | 68 | if (session.getSubscription() != null) { // Subscription was just bought 69 | if (paymentsWithSessionId.size() != 1) 70 | throw new WebHookValidationException("Received invalid webhook event (" + PaymentProcessor.STRIPE + ", wrong amount of payments (" + paymentsWithSessionId.size() + ") for subscription that was just created," + 71 | " expected 1 with stripe session id " + session.getId() + ")."); 72 | 73 | Payment firstPayment = paymentsWithSessionId.get(0); 74 | firstPayment.stripeSubscriptionId = session.getSubscription(); 75 | firstPayment.stripePaymentIntentId = Invoice.retrieve(com.stripe.model.Subscription.retrieve(session.getSubscription()).getLatestInvoice()).getPaymentIntent(); 76 | firstPayment.timestampAuthorized = now; 77 | 78 | Payment.update(firstPayment); 79 | PayHook.onPaymentAuthorized.execute(firstPayment); 80 | } else { // Authorized/Completed one-time payment(s) 81 | for (Payment payment : paymentsWithSessionId) { 82 | if (payment.timestampAuthorized == 0) { 83 | payment.timestampAuthorized = now; 84 | payment.stripePaymentIntentId = session.getPaymentIntent(); 85 | Payment.update(payment); 86 | PayHook.onPaymentAuthorized.execute(payment); 87 | } 88 | } 89 | } 90 | } else if ("invoice.created".equals(type)) {// Recurring payments 91 | // Return 2xx status code to auto-finalize the invoice and receive an invoice.paid event next 92 | } else if ("invoice.paid".equals(type)) {// Recurring payments 93 | Invoice invoice = (Invoice) stripeObject; 94 | String subscriptionId = invoice.getSubscription(); 95 | if (subscriptionId == null) 96 | return; // Make sure NOT recurring payments are ignored (handled by checkout.session.completed) 97 | if (invoice.getBillingReason().equals("subscription_create")) return; // Also ignore 98 | // the first payment/invoice for a subscription, because that MUST be handled by checkout.session.completed, 99 | // because this event has no information about the checkout session id. 100 | 101 | List authorizedPayments = Payment.getAuthorizedPayments("stripeSubscriptionId = ?", subscriptionId); 102 | if (authorizedPayments.isEmpty()) throw new WebHookValidationException( 103 | "Received invalid webhook event (" + PaymentProcessor.STRIPE + ", failed to find authorized payments with stripeSubscriptionId '" + subscriptionId + "' in local database)."); 104 | Payment lastPayment = authorizedPayments.get(authorizedPayments.size() - 1); 105 | Product product = Product.get(lastPayment.productId); 106 | if (product.charge != invoice.getAmountPaid()) 107 | throw new WebHookValidationException("Received invalid webhook event (" + PaymentProcessor.STRIPE + ", expected paid amount of '" + product.charge + "' but got '" + invoice.getAmountPaid() + "')."); 108 | Payment newPayment = lastPayment.clone(); 109 | newPayment.id = Payment.create(lastPayment.userId, invoice.getAmountPaid(), product.currency, product.paymentInterval) 110 | .id; 111 | newPayment.url = null; 112 | newPayment.charge = invoice.getAmountPaid(); 113 | newPayment.timestampCreated = now; 114 | newPayment.timestampAuthorized = now; 115 | newPayment.timestampRefunded = 0; 116 | newPayment.timestampExpires = now + 100000; 117 | newPayment.timestampCancelled = 0; 118 | Payment.add(newPayment); 119 | PayHook.onPaymentAuthorized.execute(newPayment); 120 | } else if ("customer.subscription.deleted".equals(type)) {// Recurring payments 121 | com.stripe.model.Subscription subscription = (com.stripe.model.Subscription) stripeObject; // TODO check if this actually works 122 | List payments = Payment.whereStripeSubscriptionId().is(subscription.getId()).get(); 123 | if (payments.isEmpty()) throw new WebHookValidationException( 124 | "Received invalid webhook event (" + PaymentProcessor.STRIPE + ", failed to find payments with stripe_subscription_id '" + subscription.getId() + "' in local database)."); 125 | new Subscription(payments).cancel(); 126 | } else if ("charge.refunded".equals(type)) {// Occurs whenever a charge is refunded, including partial refunds. 127 | Charge charge = (Charge) stripeObject; 128 | List payments = Payment.whereStripePaymentIntentId().is(charge.getPaymentIntent()).get(); 129 | if (payments.isEmpty()) throw new WebHookValidationException( 130 | "Received invalid webhook event (" + PaymentProcessor.STRIPE + ", failed to find payments with stripe_payment_intent_id '" + charge.getPaymentIntent() + "' in local database)."); 131 | PayHook.receiveRefund(charge.getAmount(), payments); 132 | } else { 133 | throw new WebHookValidationException("Received invalid webhook event (" + PaymentProcessor.STRIPE + ", invalid event-type: " + event.getType() + ")."); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/test/java/com/osiris/payhook/MainTest.java: -------------------------------------------------------------------------------- 1 | package com.osiris.payhook; 2 | 3 | import com.github.alexdlaird.ngrok.NgrokClient; 4 | import com.github.alexdlaird.ngrok.conf.JavaNgrokConfig; 5 | import com.github.alexdlaird.ngrok.protocol.CreateTunnel; 6 | import com.github.alexdlaird.ngrok.protocol.Tunnel; 7 | import com.google.gson.JsonElement; 8 | import com.google.gson.JsonObject; 9 | import com.osiris.dyml.Yaml; 10 | import com.osiris.jsqlgen.payhook.Payment; 11 | import com.osiris.jsqlgen.payhook.Product; 12 | import com.osiris.payhook.utils.Converter; 13 | import com.stripe.model.WebhookEndpoint; 14 | import io.muserver.*; 15 | 16 | import java.awt.*; 17 | import java.net.URI; 18 | import java.util.List; 19 | import java.util.*; 20 | import java.util.concurrent.atomic.AtomicBoolean; 21 | 22 | public class MainTest { 23 | public static SQLTestServer dbServer; 24 | public static String dbUrl; 25 | public static String dbUsername = "root"; 26 | public static String dbPassword = ""; 27 | public static String stripeSecretKey = null; 28 | private static Product pCoolCookie; 29 | private static Product pCoolSubscription; 30 | 31 | /** 32 | * A general test that tests product creation/updating 33 | * for all currently supported payment processors 34 | * and payment creation/cancellation. (sandbox mode strictly)
35 | * Note that this will delete old sandbox webhooks that contain ngrok.io in their url.
36 | */ 37 | public static void main(String[] args) throws Exception { 38 | 39 | // Fetch test credentials 40 | Yaml yaml = new Yaml(System.getProperty("user.dir") + "/test-credentials.yml"); 41 | System.out.println("Fetching credentials..."); 42 | System.out.println("File: " + yaml.file); 43 | yaml.load(); 44 | String ngrokAuthToken = yaml.put("ngrok auth token").asString(); 45 | String stripeSecretKey = yaml.put("stripe secret key").asString(); 46 | String paypalClientId = yaml.put("paypal client id").asString(); 47 | String paypalClientSecret = yaml.put("paypal client secret").asString(); 48 | yaml.save(); 49 | System.out.println("OK!"); 50 | 51 | // Test credentials check 52 | System.out.println("Checking config values (credentials cannot be null)... "); 53 | Objects.requireNonNull(ngrokAuthToken, "ngrokAuthToken cannot be null!"); 54 | Objects.requireNonNull(stripeSecretKey, "stripeSecretKey cannot be null!"); 55 | Objects.requireNonNull(paypalClientId, "paypalClientId cannot be null!"); 56 | Objects.requireNonNull(paypalClientSecret, "paypalClientSecret cannot be null!"); 57 | System.out.println("OK!"); 58 | 59 | // Init web-server to listen for webhook events, http://localhost:80/ 60 | System.out.println("Starting web-server..."); 61 | MuServer server = MuServerBuilder.httpServer() 62 | .withHttpPort(80) 63 | .addHandler(Method.GET, "/", (request, response, pathParams) -> { 64 | response.write("Currently running from " + MainTest.class); 65 | }) 66 | .addHandler(Method.POST, "/paypal-hook", MainTest::doPayPalWebhookEvent) 67 | .addHandler(Method.POST, "/stripe-hook", MainTest::doStripeWebhookEvent) 68 | .start(); 69 | System.out.println("Started web-server at " + server.uri()); 70 | System.out.println("OK!"); 71 | 72 | // Setup ngrok to tunnel traffic from public ip the current locally running service/app 73 | // Open a HTTP tunnel on the default port 80 74 | // .ngrok.io" -> "http://localhost:80"> 75 | System.out.println("Starting Ngrok-Client..."); 76 | final NgrokClient ngrokClient = new NgrokClient.Builder() 77 | .withJavaNgrokConfig(new JavaNgrokConfig.Builder().withAuthToken(ngrokAuthToken).build()) 78 | .build(); 79 | final Tunnel httpTunnel = ngrokClient.connect(new CreateTunnel.Builder().withBindTls(true).build()); 80 | String baseUrl = httpTunnel.getPublicUrl(); 81 | String stripeWebhookUrl = baseUrl + "/stripe-hook"; 82 | String paypalWebhookUrl = baseUrl + "/paypal-hook"; 83 | System.out.println("Public baseUrl: " + baseUrl); 84 | System.out.println("Public stripeWebhookUrl: " + stripeWebhookUrl); 85 | System.out.println("Public paypalWebhookUrl: " + paypalWebhookUrl); 86 | System.out.println("Now forwarding traffic from " + baseUrl + " to " + server.uri()); 87 | System.out.println("OK!"); 88 | 89 | // Init test database without password 90 | System.out.println("Starting database..."); 91 | dbServer = SQLTestServer.buildAndRun(); 92 | dbUrl = dbServer.getUrl(); 93 | System.out.println("Url: " + dbUrl); 94 | System.out.println("OK!"); 95 | 96 | // Initialise payhook 97 | System.out.println("Starting PayHook..."); 98 | PayHook.init( 99 | "Test-Brand-Name", 100 | MainTest.dbUrl, 101 | dbServer.getName(), 102 | MainTest.dbUsername, 103 | MainTest.dbPassword, 104 | true, 105 | "https://my-shop.com/payment/success", 106 | "https://my-shop.com/payment/cancel"); 107 | 108 | // Init processors 109 | PayHook.initStripe(stripeSecretKey, stripeWebhookUrl); 110 | PayHook.initPayPal(paypalClientId, paypalClientSecret, paypalWebhookUrl); 111 | 112 | // Delete old webhook endpoints that have ngrok.io in their url // STRIPE 113 | Map params = new HashMap<>(); 114 | params.put("limit", "100"); 115 | for (WebhookEndpoint webhook : 116 | WebhookEndpoint.list(params).getData()) { 117 | if (!webhook.getUrl().equals(stripeWebhookUrl) && webhook.getUrl().contains("ngrok.io")) { 118 | webhook.delete(); 119 | } 120 | } 121 | 122 | // Delete old webhook endpoints that have ngrok.io in their url // PAYPAL 123 | for (JsonElement el : PayHook.paypalUtils.getWebhooks()) { 124 | JsonObject webhook = el.getAsJsonObject(); 125 | String url = webhook.get("url").getAsString(); 126 | if (!url.equals(paypalWebhookUrl) && url.contains("ngrok.io")) { 127 | String id = webhook.get("id").getAsString(); 128 | PayHook.paypalUtils.deleteWebhook(id); 129 | } 130 | } 131 | System.out.println("Payment processors initialised with webhooks above."); 132 | 133 | 134 | // Create/Update products 135 | pCoolCookie = PayHook.putProduct(0, 500, "EUR", "Cool-Cookie", "A really yummy cookie.", Payment.Interval.NONE); 136 | pCoolSubscription = PayHook.putProduct(1, 999, "EUR", "Cool-Subscription", "A really creative description.", Payment.Interval.MONTHLY); 137 | System.out.println("Created/Updated products."); 138 | System.out.println("OK!"); 139 | 140 | 141 | /* 142 | // The below doesn't work, instead cancel the subscription in the next 143 | // run of the program 144 | List listSubscriptionsToCancel = new ArrayList<>(); 145 | Runtime.getRuntime().addShutdownHook(new Thread(() -> { 146 | for (Subscription subscription : listSubscriptionsToCancel) { 147 | try { 148 | subscription.cancel(); // Cancel all subscriptions created in this session on exit 149 | System.out.println("Waiting for cancel confirmation of subscription: " + subscription.toPrintString()); 150 | } catch (Exception e) { 151 | e.printStackTrace(); 152 | } 153 | } 154 | })); 155 | */ 156 | 157 | // Register event listeners 158 | PayHook.onPaymentAuthorized.addAction((action, payment) -> { 159 | System.out.println("(BACKEND) AUTHORIZED PAYMENT: " + payment.toPrintString()); 160 | //if (payment.isRecurring()) { 161 | // listSubscriptionsToCancel.add(new Subscription(payment)); 162 | //} 163 | }, Exception::printStackTrace); 164 | PayHook.onPaymentCancelled.addAction((action, payment) -> { 165 | System.out.println("(BACKEND) CANCELLED PAYMENT: " + payment.toPrintString()); 166 | }, Exception::printStackTrace); 167 | PayHook.onPaymentRefunded.addAction((action, payment) -> { 168 | System.out.println("(BACKEND) REFUNDED PAYMENT: " + payment.toPrintString()); 169 | }, Exception::printStackTrace); 170 | PayHook.onPaymentExpired.addAction((action, payment) -> { 171 | System.out.println("(BACKEND) EXPIRED PAYMENT: " + payment.toPrintString()); 172 | }, Exception::printStackTrace); 173 | 174 | // Check for active subscriptions and cancel them: 175 | for (Subscription sub : Subscription.getNotCancelled()) { 176 | try{ 177 | sub.cancel(); 178 | System.out.println("Cancelled active subscription: "+ sub.toPrintString()); 179 | } catch (Exception e) { 180 | e.printStackTrace(); 181 | } 182 | } 183 | 184 | // Test payments 185 | System.out.println("==========================================================================================="); 186 | System.out.println("==========================================================================================="); 187 | System.out.println("==========================================================================================="); 188 | printHelp(); 189 | System.out.println("==========================================================================================="); 190 | System.out.println("Listening for user input."); 191 | System.out.println("You can test payments (buy products) for example. Enter 'help' to list all commands."); 192 | System.out.println("Note that receiving PayPal webhook events can take up to a few minutes."); 193 | System.out.println("This is probably due to the test environment."); 194 | 195 | while (true) { 196 | String command = new Scanner(System.in).nextLine().trim(); 197 | try { 198 | if (command.equals("help")) { 199 | printHelp(); 200 | } else if (command.equals("buy cool-cookie paypal")) 201 | waitForPayment(pCoolCookie, PaymentProcessor.PAYPAL); 202 | else if (command.equals("buy cool-cookie stripe")) 203 | waitForPayment(pCoolCookie, PaymentProcessor.STRIPE); 204 | else if (command.equals("buy cool-subscription paypal")) 205 | waitForPayment(pCoolSubscription, PaymentProcessor.PAYPAL); 206 | else if (command.equals("buy cool-subscription stripe")) 207 | waitForPayment(pCoolSubscription, PaymentProcessor.STRIPE); 208 | else if (command.startsWith("cancel ")) { 209 | String paymentId = command.replace("cancel ", "").trim(); 210 | List payments = Payment.whereId().is(Integer.parseInt(paymentId)).get(); 211 | if (payments.isEmpty()) 212 | System.err.println("Payment with id '" + paymentId + "' not found in database!"); 213 | else { 214 | PayHook.cancelPayment(payments.get(0)); 215 | System.out.println("Cancelled payment!"); 216 | } 217 | } else if (command.startsWith("refund ")) { 218 | String paymentId = command.replace("refund ", "").trim(); 219 | List payments = Payment.whereId().is(Integer.parseInt(paymentId)).get(); 220 | if (payments.isEmpty()) 221 | System.err.println("Payment with id '" + paymentId + "' not found in database!"); 222 | else { 223 | PayHook.refundPayments(payments.get(0)); 224 | System.out.println("Refunded payment!"); 225 | } 226 | } else if (command.equals("print payments")) { 227 | List payments = Payment.get(); 228 | System.out.println("Showing " + payments.size() + " payments:"); 229 | for (Payment payment : payments) { 230 | System.out.println(payment.toPrintString()); 231 | } 232 | } else if (command.equals("print products")) { 233 | List products = Product.get(); 234 | System.out.println("Showing " + products.size() + " products:"); 235 | for (Product product : products) { 236 | System.out.println(product.toPrintString()); 237 | } 238 | } else if (command.equals("print subscriptions")) { 239 | for (Subscription sub : Subscription.paymentsToSubscriptions(Payment.get())) { 240 | System.out.println(sub.toPrintString()); 241 | } 242 | } else if (command.equals("print active subscriptions")) { 243 | for (Subscription sub : Subscription.getNotCancelled()) { 244 | System.out.println(sub.toPrintString()); 245 | } 246 | } else if (command.startsWith("delete payment")) { 247 | int id = Integer.parseInt(command.replace("delete payment ", "").trim()); 248 | Payment.whereId().is(id).remove(); 249 | } else if (command.startsWith("delete product")) { 250 | int id = Integer.parseInt(command.replace("delete product ", "").trim()); 251 | Product.whereId().is(id).remove(); 252 | } else 253 | System.err.println("Unknown command '" + command + "', please enter a valid one."); 254 | } catch (Exception e) { 255 | e.printStackTrace(); 256 | System.err.println("Something went wrong during command execution. See details above."); 257 | } 258 | } 259 | } 260 | 261 | private static void printHelp() { 262 | System.out.println("Available commands:"); 263 | System.out.println(); 264 | System.out.println("buy cool-cookie paypal"); 265 | System.out.println("buy cool-cookie stripe"); 266 | System.out.println("buy cool-subscription paypal"); 267 | System.out.println("buy cool-subscription stripe"); 268 | System.out.println(); 269 | System.out.println("refund "); 270 | System.out.println("cancel "); 271 | System.out.println(); 272 | System.out.println("print payments"); 273 | System.out.println("print products"); 274 | System.out.println("print subscriptions"); 275 | System.out.println("print active subscriptions"); 276 | System.out.println(); 277 | System.out.println("delete payment "); 278 | System.out.println("delete product "); 279 | } 280 | 281 | private static void waitForPayment(Product product, PaymentProcessor paymentProcessor) throws Exception { 282 | AtomicBoolean isAuthorized = new AtomicBoolean(false); 283 | System.out.println("Buying " + product.name + " over " + paymentProcessor + "."); 284 | Payment payment = PayHook.expectPayment("testUser", product, paymentProcessor, 285 | authorizedPayment -> { 286 | isAuthorized.set(true); 287 | // OPTIONAL -- 288 | // Insert ONLY additional UI code here (make sure to have access to the UI thread). 289 | // Code that does backend, aka important stuff does not belong here! 290 | // Gets executed when the payment was authorized. 291 | System.out.println("Received authorized payment for " + 292 | authorizedPayment.productName + " " + new Converter().toMoneyString(authorizedPayment.currency, authorizedPayment.charge)); 293 | }, 294 | cancelledPayment -> { 295 | // OPTIONAL -- 296 | // Insert ONLY additional UI code here (make sure to have access to the UI thread). 297 | // Code that does backend, aka important stuff does not belong here! 298 | // Gets executed when the payment was cancelled. 299 | System.out.println("Cancelled payment for " + 300 | cancelledPayment.productName + " " + new Converter().toMoneyString(cancelledPayment.currency, cancelledPayment.charge)); 301 | }); 302 | 303 | System.out.println("Authorize payment here: " + payment.url); 304 | System.out.println("Waiting for you to authorize the payment..."); 305 | if (Desktop.isDesktopSupported()) 306 | Desktop.getDesktop().browse(URI.create(payment.url)); 307 | while (!isAuthorized.get()) Thread.sleep(100); 308 | } 309 | 310 | private static void doPayPalWebhookEvent(MuRequest request, MuResponse response, Map pathParams) { 311 | try { 312 | response.status(200); // Directly set status code 313 | PayHook.receiveWebhookEvent( 314 | PaymentProcessor.PAYPAL, 315 | getHeadersAsMap(request), 316 | request.readBodyAsString()); 317 | } catch (Exception e) { 318 | e.printStackTrace(); 319 | // TODO handle exception 320 | } 321 | } 322 | 323 | private static void doStripeWebhookEvent(MuRequest request, MuResponse response, Map pathParams) { 324 | try { 325 | response.status(200); // Directly set status code 326 | PayHook.receiveWebhookEvent( 327 | PaymentProcessor.STRIPE, 328 | getHeadersAsMap(request), 329 | request.readBodyAsString()); 330 | } catch (Exception e) { 331 | e.printStackTrace(); 332 | // TODO handle exception 333 | } 334 | } 335 | 336 | // Simple helper method to help you extract the headers from HttpServletRequest object. 337 | private static Map getHeadersAsMap(MuRequest request) { 338 | Map map = new HashMap(); 339 | request.headers().forEach(e -> { 340 | map.put(e.getKey(), e.getValue()); 341 | }); 342 | return map; 343 | } 344 | } 345 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/jsqlgen/payhook/WebhookEndpoint.java: -------------------------------------------------------------------------------- 1 | package com.osiris.jsqlgen.payhook; 2 | 3 | import java.sql.Connection; 4 | import java.sql.PreparedStatement; 5 | import java.sql.ResultSet; 6 | import java.sql.Statement; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.function.Consumer; 10 | 11 | /** 12 | Generated class by jSQL-Gen 13 | that contains static methods for fetching/updating data from the "WebhookEndpoint" table. 14 | A single object/instance of this class represents a single row in the table 15 | and data can be accessed via its public fields.

16 | Its not recommended to modify this class but it should be OK to add new methods to it. 17 | If modifications are really needed create a pull request directly to jSQL-Gen instead.
18 | NO EXCEPTIONS is enabled which makes it possible to use this methods outside of try/catch blocks because SQL errors will be caught and thrown as runtime exceptions instead.
19 | */ 20 | public class WebhookEndpoint{ 21 | public static java.util.concurrent.atomic.AtomicInteger idCounter = new java.util.concurrent.atomic.AtomicInteger(0); 22 | static { 23 | try{ 24 | Connection con = Database.getCon(); 25 | try{ 26 | try (Statement s = con.createStatement()) { 27 | s.executeUpdate("CREATE TABLE IF NOT EXISTS `webhookendpoint` (`id` INT NOT NULL PRIMARY KEY)"); 28 | try{s.executeUpdate("ALTER TABLE `webhookendpoint` ADD COLUMN `url` TEXT NOT NULL");}catch(Exception ignored){} 29 | s.executeUpdate("ALTER TABLE `webhookendpoint` MODIFY COLUMN `url` TEXT NOT NULL"); 30 | try{s.executeUpdate("ALTER TABLE `webhookendpoint` ADD COLUMN `stripeWebhookSecret` TEXT NOT NULL");}catch(Exception ignored){} 31 | s.executeUpdate("ALTER TABLE `webhookendpoint` MODIFY COLUMN `stripeWebhookSecret` TEXT NOT NULL"); 32 | } 33 | try (PreparedStatement ps = con.prepareStatement("SELECT id FROM `webhookendpoint` ORDER BY id DESC LIMIT 1")) { 34 | ResultSet rs = ps.executeQuery(); 35 | if (rs.next()) idCounter.set(rs.getInt(1) + 1); 36 | } 37 | } 38 | catch(Exception e){ throw new RuntimeException(e); } 39 | finally {Database.freeCon(con);} 40 | }catch(Exception e){ 41 | e.printStackTrace(); 42 | System.err.println("Something went really wrong during table (WebhookEndpoint) initialisation, thus the program will exit!");System.exit(1);} 43 | } 44 | 45 | /** 46 | Use the static create method instead of this constructor, 47 | if you plan to add this object to the database in the future, since 48 | that method fetches and sets/reserves the {@link #id}. 49 | */ 50 | public WebhookEndpoint (int id, String url, String stripeWebhookSecret){ 51 | this.id = id;this.url = url;this.stripeWebhookSecret = stripeWebhookSecret; 52 | } 53 | /** 54 | Database field/value. Not null.
55 | */ 56 | public int id; 57 | /** 58 | Database field/value. Not null.
59 | */ 60 | public String url; 61 | /** 62 | Database field/value. Not null.
63 | */ 64 | public String stripeWebhookSecret; 65 | /** 66 | Creates and returns an object that can be added to this table. 67 | Increments the id (thread-safe) and sets it for this object (basically reserves a space in the database). 68 | Note that the parameters of this method represent "NOT NULL" fields in the table and thus should not be null. 69 | Also note that this method will NOT add the object to the table. 70 | */ 71 | public static WebhookEndpoint create( String url, String stripeWebhookSecret) { 72 | int id = idCounter.getAndIncrement(); 73 | WebhookEndpoint obj = new WebhookEndpoint(id, url, stripeWebhookSecret); 74 | return obj; 75 | } 76 | 77 | /** 78 | Convenience method for creating and directly adding a new object to the table. 79 | Note that the parameters of this method represent "NOT NULL" fields in the table and thus should not be null. 80 | */ 81 | public static WebhookEndpoint createAndAdd( String url, String stripeWebhookSecret) { 82 | int id = idCounter.getAndIncrement(); 83 | WebhookEndpoint obj = new WebhookEndpoint(id, url, stripeWebhookSecret); 84 | add(obj); 85 | return obj; 86 | } 87 | 88 | /** 89 | @return a list containing all objects in this table. 90 | */ 91 | public static List get() {return get(null);} 92 | /** 93 | @return object with the provided id or null if there is no object with the provided id in this table. 94 | @throws Exception on SQL issues. 95 | */ 96 | public static WebhookEndpoint get(int id) { 97 | try{ 98 | return get("WHERE id = "+id).get(0); 99 | }catch(IndexOutOfBoundsException ignored){} 100 | catch(Exception e){throw new RuntimeException(e);} 101 | return null; 102 | } 103 | /** 104 | Example:
105 | get("WHERE username=? AND age=?", "Peter", 33);
106 | @param where can be null. Your SQL WHERE statement (with the leading WHERE). 107 | @param whereValues can be null. Your SQL WHERE statement values to set for '?'. 108 | @return a list containing only objects that match the provided SQL WHERE statement (no matches = empty list). 109 | if that statement is null, returns all the contents of this table. 110 | */ 111 | public static List get(String where, Object... whereValues) { 112 | String sql = "SELECT `id`,`url`,`stripeWebhookSecret`" + 113 | " FROM `webhookendpoint`" + 114 | (where != null ? where : ""); 115 | List list = new ArrayList<>(); 116 | Connection con = Database.getCon(); 117 | try (PreparedStatement ps = con.prepareStatement(sql)) { 118 | if(where!=null && whereValues!=null) 119 | for (int i = 0; i < whereValues.length; i++) { 120 | Object val = whereValues[i]; 121 | ps.setObject(i+1, val); 122 | } 123 | ResultSet rs = ps.executeQuery(); 124 | while (rs.next()) { 125 | WebhookEndpoint obj = new WebhookEndpoint(); 126 | list.add(obj); 127 | obj.id = rs.getInt(1); 128 | obj.url = rs.getString(2); 129 | obj.stripeWebhookSecret = rs.getString(3); 130 | } 131 | }catch(Exception e){throw new RuntimeException(e);} 132 | finally{Database.freeCon(con);} 133 | return list; 134 | } 135 | 136 | /** 137 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 138 | */ 139 | public static void getLazy(Consumer> onResultReceived){ 140 | getLazy(onResultReceived, null, 500, null); 141 | } 142 | /** 143 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 144 | */ 145 | public static void getLazy(Consumer> onResultReceived, int limit){ 146 | getLazy(onResultReceived, null, limit, null); 147 | } 148 | /** 149 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 150 | */ 151 | public static void getLazy(Consumer> onResultReceived, Consumer onFinish){ 152 | getLazy(onResultReceived, onFinish, 500, null); 153 | } 154 | /** 155 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 156 | */ 157 | public static void getLazy(Consumer> onResultReceived, Consumer onFinish, int limit){ 158 | getLazy(onResultReceived, onFinish, limit, null); 159 | } 160 | /** 161 | * Loads results lazily in a new thread.
162 | * Add {@link Thread#sleep(long)} at the end of your onResultReceived code, to sleep between fetches. 163 | * @param onResultReceived can NOT be null. Gets executed until there are no results left, thus the results list is never empty. 164 | * @param onFinish can be null. Gets executed when finished receiving all results. Provides the total amount of received elements as parameter. 165 | * @param limit the maximum amount of elements for each fetch. 166 | * @param where can be null. This WHERE is not allowed to contain LIMIT and should not contain order by id. 167 | */ 168 | public static void getLazy(Consumer> onResultReceived, Consumer onFinish, int limit, WHERE where) { 169 | new Thread(() -> { 170 | WHERE finalWhere; 171 | if(where == null) finalWhere = new WHERE(""); 172 | else finalWhere = where; 173 | List results; 174 | int lastId = -1; 175 | long count = 0; 176 | while(true){ 177 | results = whereId().biggerThan(lastId).and(finalWhere).limit(limit).get(); 178 | if(results.isEmpty()) break; 179 | lastId = results.get(results.size() - 1).id; 180 | count += results.size(); 181 | onResultReceived.accept(results); 182 | } 183 | if(onFinish!=null) onFinish.accept(count); 184 | }).start(); 185 | } 186 | 187 | public static int count(){ return count(null, null); } 188 | 189 | public static int count(String where, Object... whereValues) { 190 | String sql = "SELECT COUNT(`id`) AS recordCount FROM `webhookendpoint`" + 191 | (where != null ? where : ""); 192 | Connection con = Database.getCon(); 193 | try (PreparedStatement ps = con.prepareStatement(sql)) { 194 | if(where!=null && whereValues!=null) 195 | for (int i = 0; i < whereValues.length; i++) { 196 | Object val = whereValues[i]; 197 | ps.setObject(i+1, val); 198 | } 199 | ResultSet rs = ps.executeQuery(); 200 | if (rs.next()) return rs.getInt("recordCount"); 201 | }catch(Exception e){throw new RuntimeException(e);} 202 | finally {Database.freeCon(con);} 203 | return 0; 204 | } 205 | 206 | /** 207 | Searches the provided object in the database (by its id), 208 | and updates all its fields. 209 | @throws Exception when failed to find by id or other SQL issues. 210 | */ 211 | public static void update(WebhookEndpoint obj) { 212 | String sql = "UPDATE `webhookendpoint` SET `id`=?,`url`=?,`stripeWebhookSecret`=? WHERE id="+obj.id; 213 | Connection con = Database.getCon(); 214 | try (PreparedStatement ps = con.prepareStatement(sql)) { 215 | ps.setInt(1, obj.id); 216 | ps.setString(2, obj.url); 217 | ps.setString(3, obj.stripeWebhookSecret); 218 | ps.executeUpdate(); 219 | }catch(Exception e){throw new RuntimeException(e);} 220 | finally{Database.freeCon(con);} 221 | } 222 | 223 | /** 224 | Adds the provided object to the database (note that the id is not checked for duplicates). 225 | */ 226 | public static void add(WebhookEndpoint obj) { 227 | String sql = "INSERT INTO `webhookendpoint` (`id`,`url`,`stripeWebhookSecret`) VALUES (?,?,?)"; 228 | Connection con = Database.getCon(); 229 | try (PreparedStatement ps = con.prepareStatement(sql)) { 230 | ps.setInt(1, obj.id); 231 | ps.setString(2, obj.url); 232 | ps.setString(3, obj.stripeWebhookSecret); 233 | ps.executeUpdate(); 234 | }catch(Exception e){throw new RuntimeException(e);} 235 | finally{Database.freeCon(con);} 236 | } 237 | 238 | /** 239 | Deletes the provided object from the database. 240 | */ 241 | public static void remove(WebhookEndpoint obj) { 242 | remove("WHERE id = "+obj.id); 243 | } 244 | /** 245 | Example:
246 | remove("WHERE username=?", "Peter");
247 | Deletes the objects that are found by the provided SQL WHERE statement, from the database. 248 | @param where can NOT be null. 249 | @param whereValues can be null. Your SQL WHERE statement values to set for '?'. 250 | */ 251 | public static void remove(String where, Object... whereValues) { 252 | java.util.Objects.requireNonNull(where); 253 | String sql = "DELETE FROM `webhookendpoint` "+where; 254 | Connection con = Database.getCon(); 255 | try (PreparedStatement ps = con.prepareStatement(sql)) { 256 | if(whereValues != null) 257 | for (int i = 0; i < whereValues.length; i++) { 258 | Object val = whereValues[i]; 259 | ps.setObject(i+1, val); 260 | } 261 | ps.executeUpdate(); 262 | }catch(Exception e){throw new RuntimeException(e);} 263 | finally{Database.freeCon(con);} 264 | } 265 | 266 | public static void removeAll() { 267 | String sql = "DELETE FROM `webhookendpoint`"; 268 | Connection con = Database.getCon(); 269 | try (PreparedStatement ps = con.prepareStatement(sql)) { 270 | ps.executeUpdate(); 271 | }catch(Exception e){throw new RuntimeException(e);} 272 | finally{Database.freeCon(con);} 273 | } 274 | 275 | public WebhookEndpoint clone(){ 276 | return new WebhookEndpoint(this.id,this.url,this.stripeWebhookSecret); 277 | } 278 | public String toPrintString(){ 279 | return ""+"id="+this.id+" "+"url="+this.url+" "+"stripeWebhookSecret="+this.stripeWebhookSecret+" "; 280 | } 281 | public static WHERE whereId() { 282 | return new WHERE("`id`"); 283 | } 284 | public static WHERE whereUrl() { 285 | return new WHERE("`url`"); 286 | } 287 | public static WHERE whereStripeWebhookSecret() { 288 | return new WHERE("`stripeWebhookSecret`"); 289 | } 290 | public static class WHERE { 291 | /** 292 | * Remember to prepend WHERE on the final SQL statement. 293 | * This is not done by this class due to performance reasons.

294 | *

295 | * Note that it excepts the generated SQL string to be used by a {@link java.sql.PreparedStatement} 296 | * to protect against SQL-Injection.

297 | *

298 | * Also note that the SQL query gets optimized by the database automatically, 299 | * thus It's recommended to make queries as readable as possible and 300 | * not worry that much about performance. 301 | */ 302 | public StringBuilder sqlBuilder = new StringBuilder(); 303 | public StringBuilder orderByBuilder = new StringBuilder(); 304 | public StringBuilder limitBuilder = new StringBuilder(); 305 | List whereObjects = new ArrayList<>(); 306 | private final String columnName; 307 | public WHERE(String columnName) { 308 | this.columnName = columnName; 309 | } 310 | 311 | /** 312 | * Executes the generated SQL statement 313 | * and returns a list of objects matching the query. 314 | */ 315 | public List get() { 316 | String where = sqlBuilder.toString(); 317 | if(!where.isEmpty()) where = " WHERE " + where; 318 | String orderBy = orderByBuilder.toString(); 319 | if(!orderBy.isEmpty()) orderBy = " ORDER BY "+orderBy.substring(0, orderBy.length()-2)+" "; 320 | if(!whereObjects.isEmpty()) 321 | return WebhookEndpoint.get(where+orderBy+limitBuilder.toString(), whereObjects.toArray()); 322 | else 323 | return WebhookEndpoint.get(where+orderBy+limitBuilder.toString(), (T[]) null); 324 | } 325 | 326 | /** 327 | * Executes the generated SQL statement 328 | * and returns the size of the list of objects matching the query. 329 | */ 330 | public int count() { 331 | String where = sqlBuilder.toString(); 332 | if(!where.isEmpty()) where = " WHERE " + where; 333 | String orderBy = orderByBuilder.toString(); 334 | if(!orderBy.isEmpty()) orderBy = " ORDER BY "+orderBy.substring(0, orderBy.length()-2)+" "; 335 | if(!whereObjects.isEmpty()) 336 | return WebhookEndpoint.count(where+orderBy+limitBuilder.toString(), whereObjects.toArray()); 337 | else 338 | return WebhookEndpoint.count(where+orderBy+limitBuilder.toString(), (T[]) null); 339 | } 340 | 341 | /** 342 | * Executes the generated SQL statement 343 | * and removes the objects matching the query. 344 | */ 345 | public void remove() { 346 | String where = sqlBuilder.toString(); 347 | if(!where.isEmpty()) where = " WHERE " + where; 348 | String orderBy = orderByBuilder.toString(); 349 | if(!orderBy.isEmpty()) orderBy = " ORDER BY "+orderBy.substring(0, orderBy.length()-2)+" "; 350 | if(!whereObjects.isEmpty()) 351 | WebhookEndpoint.remove(where+orderBy+limitBuilder.toString(), whereObjects.toArray()); 352 | else 353 | WebhookEndpoint.remove(where+orderBy+limitBuilder.toString(), (T[]) null); 354 | } 355 | 356 | /** 357 | * AND (...)
358 | */ 359 | public WHERE and(WHERE where) { 360 | String sql = where.sqlBuilder.toString(); 361 | if(!sql.isEmpty()) { 362 | sqlBuilder.append("AND (").append(sql).append(") "); 363 | whereObjects.addAll(where.whereObjects); 364 | } 365 | orderByBuilder.append(where.orderByBuilder.toString()); 366 | return this; 367 | } 368 | 369 | /** 370 | * OR (...)
371 | */ 372 | public WHERE or(WHERE where) { 373 | String sql = where.sqlBuilder.toString(); 374 | if(!sql.isEmpty()) { 375 | sqlBuilder.append("OR (").append(sql).append(") "); 376 | whereObjects.addAll(where.whereObjects); 377 | } 378 | orderByBuilder.append(where.orderByBuilder.toString()); 379 | return this; 380 | } 381 | 382 | /** 383 | * columnName = ?
384 | */ 385 | public WHERE is(T obj) { 386 | sqlBuilder.append(columnName).append(" = ? "); 387 | whereObjects.add(obj); 388 | return this; 389 | } 390 | 391 | /** 392 | * columnName IN (?,?,...)
393 | * 394 | * @see https://www.w3schools.com/mysql/mysql_in.asp 395 | */ 396 | public WHERE is(T... objects) { 397 | String s = ""; 398 | for (T obj : objects) { 399 | s += "?,"; 400 | whereObjects.add(obj); 401 | } 402 | s = s.substring(0, s.length() - 1); // Remove last , 403 | sqlBuilder.append(columnName).append(" IN (" + s + ") "); 404 | return this; 405 | } 406 | 407 | /** 408 | * columnName <> ?
409 | */ 410 | public WHERE isNot(T obj) { 411 | sqlBuilder.append(columnName).append(" <> ? "); 412 | whereObjects.add(obj); 413 | return this; 414 | } 415 | 416 | /** 417 | * columnName IS NULL
418 | */ 419 | public WHERE isNull() { 420 | sqlBuilder.append(columnName).append(" IS NULL "); 421 | return this; 422 | } 423 | 424 | /** 425 | * columnName IS NOT NULL
426 | */ 427 | public WHERE isNotNull() { 428 | sqlBuilder.append(columnName).append(" IS NOT NULL "); 429 | return this; 430 | } 431 | 432 | /** 433 | * columnName LIKE ?
434 | * 435 | * @see https://www.w3schools.com/mysql/mysql_like.asp 436 | */ 437 | public WHERE like(T obj) { 438 | sqlBuilder.append(columnName).append(" LIKE ? "); 439 | whereObjects.add(obj); 440 | return this; 441 | } 442 | 443 | /** 444 | * columnName NOT LIKE ?
445 | * 446 | * @see https://www.w3schools.com/mysql/mysql_like.asp 447 | */ 448 | public WHERE notLike(T obj) { 449 | sqlBuilder.append(columnName).append(" NOT LIKE ? "); 450 | whereObjects.add(obj); 451 | return this; 452 | } 453 | 454 | /** 455 | * columnName > ?
456 | */ 457 | public WHERE biggerThan(T obj) { 458 | sqlBuilder.append(columnName).append(" > ? "); 459 | whereObjects.add(obj); 460 | return this; 461 | } 462 | 463 | /** 464 | * columnName < ?
465 | */ 466 | public WHERE smallerThan(T obj) { 467 | sqlBuilder.append(columnName).append(" < ? "); 468 | whereObjects.add(obj); 469 | return this; 470 | } 471 | 472 | /** 473 | * columnName >= ?
474 | */ 475 | public WHERE biggerOrEqual(T obj) { 476 | sqlBuilder.append(columnName).append(" >= ? "); 477 | whereObjects.add(obj); 478 | return this; 479 | } 480 | 481 | /** 482 | * columnName <= ?
483 | */ 484 | public WHERE smallerOrEqual(T obj) { 485 | sqlBuilder.append(columnName).append(" <= ? "); 486 | whereObjects.add(obj); 487 | return this; 488 | } 489 | 490 | /** 491 | * columnName BETWEEN ? AND ?
492 | */ 493 | public WHERE between(T obj1, T obj2) { 494 | sqlBuilder.append(columnName).append(" BETWEEN ? AND ? "); 495 | whereObjects.add(obj1); 496 | whereObjects.add(obj2); 497 | return this; 498 | } 499 | 500 | /** 501 | * columnName ASC,
502 | * 503 | * @see https://www.w3schools.com/mysql/mysql_like.asp 504 | */ 505 | public WHERE smallestFirst() { 506 | orderByBuilder.append(columnName + " ASC, "); 507 | return this; 508 | } 509 | 510 | /** 511 | * columnName DESC,
512 | * 513 | * @see https://www.w3schools.com/mysql/mysql_like.asp 514 | */ 515 | public WHERE biggestFirst() { 516 | orderByBuilder.append(columnName + " DESC, "); 517 | return this; 518 | } 519 | 520 | /** 521 | * LIMIT number
522 | * 523 | * @see https://www.w3schools.com/mysql/mysql_limit.asp 524 | */ 525 | public WHERE limit(int num) { 526 | limitBuilder.append("LIMIT ").append(num + " "); 527 | return this; 528 | } 529 | 530 | } 531 | // The code below will not be removed when re-generating this class. 532 | // Additional code start -> 533 | private WebhookEndpoint(){} 534 | // Additional code end <- 535 | } 536 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/jsqlgen/payhook/PendingPaymentCancel.java: -------------------------------------------------------------------------------- 1 | package com.osiris.jsqlgen.payhook; 2 | 3 | import java.sql.Connection; 4 | import java.sql.PreparedStatement; 5 | import java.sql.ResultSet; 6 | import java.sql.Statement; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.function.Consumer; 10 | 11 | /** 12 | Generated class by jSQL-Gen 13 | that contains static methods for fetching/updating data from the "PendingPaymentCancel" table. 14 | A single object/instance of this class represents a single row in the table 15 | and data can be accessed via its public fields.

16 | Its not recommended to modify this class but it should be OK to add new methods to it. 17 | If modifications are really needed create a pull request directly to jSQL-Gen instead.
18 | NO EXCEPTIONS is enabled which makes it possible to use this methods outside of try/catch blocks because SQL errors will be caught and thrown as runtime exceptions instead.
19 | */ 20 | public class PendingPaymentCancel{ 21 | public static java.util.concurrent.atomic.AtomicInteger idCounter = new java.util.concurrent.atomic.AtomicInteger(0); 22 | static { 23 | try{ 24 | Connection con = Database.getCon(); 25 | try{ 26 | try (Statement s = con.createStatement()) { 27 | s.executeUpdate("CREATE TABLE IF NOT EXISTS `pendingpaymentcancel` (`id` INT NOT NULL PRIMARY KEY)"); 28 | try{s.executeUpdate("ALTER TABLE `pendingpaymentcancel` ADD COLUMN `paymentId` INT NOT NULL");}catch(Exception ignored){} 29 | s.executeUpdate("ALTER TABLE `pendingpaymentcancel` MODIFY COLUMN `paymentId` INT NOT NULL"); 30 | try{s.executeUpdate("ALTER TABLE `pendingpaymentcancel` ADD COLUMN `timestampCancel` BIGINT NOT NULL");}catch(Exception ignored){} 31 | s.executeUpdate("ALTER TABLE `pendingpaymentcancel` MODIFY COLUMN `timestampCancel` BIGINT NOT NULL"); 32 | } 33 | try (PreparedStatement ps = con.prepareStatement("SELECT id FROM `pendingpaymentcancel` ORDER BY id DESC LIMIT 1")) { 34 | ResultSet rs = ps.executeQuery(); 35 | if (rs.next()) idCounter.set(rs.getInt(1) + 1); 36 | } 37 | } 38 | catch(Exception e){ throw new RuntimeException(e); } 39 | finally {Database.freeCon(con);} 40 | }catch(Exception e){ 41 | e.printStackTrace(); 42 | System.err.println("Something went really wrong during table (PendingPaymentCancel) initialisation, thus the program will exit!");System.exit(1);} 43 | } 44 | 45 | /** 46 | Use the static create method instead of this constructor, 47 | if you plan to add this object to the database in the future, since 48 | that method fetches and sets/reserves the {@link #id}. 49 | */ 50 | public PendingPaymentCancel (int id, int paymentId, long timestampCancel){ 51 | this.id = id;this.paymentId = paymentId;this.timestampCancel = timestampCancel; 52 | } 53 | /** 54 | Database field/value. Not null.
55 | */ 56 | public int id; 57 | /** 58 | Database field/value. Not null.
59 | */ 60 | public int paymentId; 61 | /** 62 | Database field/value. Not null.
63 | */ 64 | public long timestampCancel; 65 | /** 66 | Creates and returns an object that can be added to this table. 67 | Increments the id (thread-safe) and sets it for this object (basically reserves a space in the database). 68 | Note that the parameters of this method represent "NOT NULL" fields in the table and thus should not be null. 69 | Also note that this method will NOT add the object to the table. 70 | */ 71 | public static PendingPaymentCancel create( int paymentId, long timestampCancel) { 72 | int id = idCounter.getAndIncrement(); 73 | PendingPaymentCancel obj = new PendingPaymentCancel(id, paymentId, timestampCancel); 74 | return obj; 75 | } 76 | 77 | /** 78 | Convenience method for creating and directly adding a new object to the table. 79 | Note that the parameters of this method represent "NOT NULL" fields in the table and thus should not be null. 80 | */ 81 | public static PendingPaymentCancel createAndAdd( int paymentId, long timestampCancel) { 82 | int id = idCounter.getAndIncrement(); 83 | PendingPaymentCancel obj = new PendingPaymentCancel(id, paymentId, timestampCancel); 84 | add(obj); 85 | return obj; 86 | } 87 | 88 | /** 89 | @return a list containing all objects in this table. 90 | */ 91 | public static List get() {return get(null);} 92 | /** 93 | @return object with the provided id or null if there is no object with the provided id in this table. 94 | @throws Exception on SQL issues. 95 | */ 96 | public static PendingPaymentCancel get(int id) { 97 | try{ 98 | return get("WHERE id = "+id).get(0); 99 | }catch(IndexOutOfBoundsException ignored){} 100 | catch(Exception e){throw new RuntimeException(e);} 101 | return null; 102 | } 103 | /** 104 | Example:
105 | get("WHERE username=? AND age=?", "Peter", 33);
106 | @param where can be null. Your SQL WHERE statement (with the leading WHERE). 107 | @param whereValues can be null. Your SQL WHERE statement values to set for '?'. 108 | @return a list containing only objects that match the provided SQL WHERE statement (no matches = empty list). 109 | if that statement is null, returns all the contents of this table. 110 | */ 111 | public static List get(String where, Object... whereValues) { 112 | String sql = "SELECT `id`,`paymentId`,`timestampCancel`" + 113 | " FROM `pendingpaymentcancel`" + 114 | (where != null ? where : ""); 115 | List list = new ArrayList<>(); 116 | Connection con = Database.getCon(); 117 | try (PreparedStatement ps = con.prepareStatement(sql)) { 118 | if(where!=null && whereValues!=null) 119 | for (int i = 0; i < whereValues.length; i++) { 120 | Object val = whereValues[i]; 121 | ps.setObject(i+1, val); 122 | } 123 | ResultSet rs = ps.executeQuery(); 124 | while (rs.next()) { 125 | PendingPaymentCancel obj = new PendingPaymentCancel(); 126 | list.add(obj); 127 | obj.id = rs.getInt(1); 128 | obj.paymentId = rs.getInt(2); 129 | obj.timestampCancel = rs.getLong(3); 130 | } 131 | }catch(Exception e){throw new RuntimeException(e);} 132 | finally{Database.freeCon(con);} 133 | return list; 134 | } 135 | 136 | /** 137 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 138 | */ 139 | public static void getLazy(Consumer> onResultReceived){ 140 | getLazy(onResultReceived, null, 500, null); 141 | } 142 | /** 143 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 144 | */ 145 | public static void getLazy(Consumer> onResultReceived, int limit){ 146 | getLazy(onResultReceived, null, limit, null); 147 | } 148 | /** 149 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 150 | */ 151 | public static void getLazy(Consumer> onResultReceived, Consumer onFinish){ 152 | getLazy(onResultReceived, onFinish, 500, null); 153 | } 154 | /** 155 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 156 | */ 157 | public static void getLazy(Consumer> onResultReceived, Consumer onFinish, int limit){ 158 | getLazy(onResultReceived, onFinish, limit, null); 159 | } 160 | /** 161 | * Loads results lazily in a new thread.
162 | * Add {@link Thread#sleep(long)} at the end of your onResultReceived code, to sleep between fetches. 163 | * @param onResultReceived can NOT be null. Gets executed until there are no results left, thus the results list is never empty. 164 | * @param onFinish can be null. Gets executed when finished receiving all results. Provides the total amount of received elements as parameter. 165 | * @param limit the maximum amount of elements for each fetch. 166 | * @param where can be null. This WHERE is not allowed to contain LIMIT and should not contain order by id. 167 | */ 168 | public static void getLazy(Consumer> onResultReceived, Consumer onFinish, int limit, WHERE where) { 169 | new Thread(() -> { 170 | WHERE finalWhere; 171 | if(where == null) finalWhere = new WHERE(""); 172 | else finalWhere = where; 173 | List results; 174 | int lastId = -1; 175 | long count = 0; 176 | while(true){ 177 | results = whereId().biggerThan(lastId).and(finalWhere).limit(limit).get(); 178 | if(results.isEmpty()) break; 179 | lastId = results.get(results.size() - 1).id; 180 | count += results.size(); 181 | onResultReceived.accept(results); 182 | } 183 | if(onFinish!=null) onFinish.accept(count); 184 | }).start(); 185 | } 186 | 187 | public static int count(){ return count(null, null); } 188 | 189 | public static int count(String where, Object... whereValues) { 190 | String sql = "SELECT COUNT(`id`) AS recordCount FROM `pendingpaymentcancel`" + 191 | (where != null ? where : ""); 192 | Connection con = Database.getCon(); 193 | try (PreparedStatement ps = con.prepareStatement(sql)) { 194 | if(where!=null && whereValues!=null) 195 | for (int i = 0; i < whereValues.length; i++) { 196 | Object val = whereValues[i]; 197 | ps.setObject(i+1, val); 198 | } 199 | ResultSet rs = ps.executeQuery(); 200 | if (rs.next()) return rs.getInt("recordCount"); 201 | }catch(Exception e){throw new RuntimeException(e);} 202 | finally {Database.freeCon(con);} 203 | return 0; 204 | } 205 | 206 | /** 207 | Searches the provided object in the database (by its id), 208 | and updates all its fields. 209 | @throws Exception when failed to find by id or other SQL issues. 210 | */ 211 | public static void update(PendingPaymentCancel obj) { 212 | String sql = "UPDATE `pendingpaymentcancel` SET `id`=?,`paymentId`=?,`timestampCancel`=? WHERE id="+obj.id; 213 | Connection con = Database.getCon(); 214 | try (PreparedStatement ps = con.prepareStatement(sql)) { 215 | ps.setInt(1, obj.id); 216 | ps.setInt(2, obj.paymentId); 217 | ps.setLong(3, obj.timestampCancel); 218 | ps.executeUpdate(); 219 | }catch(Exception e){throw new RuntimeException(e);} 220 | finally{Database.freeCon(con);} 221 | } 222 | 223 | /** 224 | Adds the provided object to the database (note that the id is not checked for duplicates). 225 | */ 226 | public static void add(PendingPaymentCancel obj) { 227 | String sql = "INSERT INTO `pendingpaymentcancel` (`id`,`paymentId`,`timestampCancel`) VALUES (?,?,?)"; 228 | Connection con = Database.getCon(); 229 | try (PreparedStatement ps = con.prepareStatement(sql)) { 230 | ps.setInt(1, obj.id); 231 | ps.setInt(2, obj.paymentId); 232 | ps.setLong(3, obj.timestampCancel); 233 | ps.executeUpdate(); 234 | }catch(Exception e){throw new RuntimeException(e);} 235 | finally{Database.freeCon(con);} 236 | } 237 | 238 | /** 239 | Deletes the provided object from the database. 240 | */ 241 | public static void remove(PendingPaymentCancel obj) { 242 | remove("WHERE id = "+obj.id); 243 | } 244 | /** 245 | Example:
246 | remove("WHERE username=?", "Peter");
247 | Deletes the objects that are found by the provided SQL WHERE statement, from the database. 248 | @param where can NOT be null. 249 | @param whereValues can be null. Your SQL WHERE statement values to set for '?'. 250 | */ 251 | public static void remove(String where, Object... whereValues) { 252 | java.util.Objects.requireNonNull(where); 253 | String sql = "DELETE FROM `pendingpaymentcancel` "+where; 254 | Connection con = Database.getCon(); 255 | try (PreparedStatement ps = con.prepareStatement(sql)) { 256 | if(whereValues != null) 257 | for (int i = 0; i < whereValues.length; i++) { 258 | Object val = whereValues[i]; 259 | ps.setObject(i+1, val); 260 | } 261 | ps.executeUpdate(); 262 | }catch(Exception e){throw new RuntimeException(e);} 263 | finally{Database.freeCon(con);} 264 | } 265 | 266 | public static void removeAll() { 267 | String sql = "DELETE FROM `pendingpaymentcancel`"; 268 | Connection con = Database.getCon(); 269 | try (PreparedStatement ps = con.prepareStatement(sql)) { 270 | ps.executeUpdate(); 271 | }catch(Exception e){throw new RuntimeException(e);} 272 | finally{Database.freeCon(con);} 273 | } 274 | 275 | public PendingPaymentCancel clone(){ 276 | return new PendingPaymentCancel(this.id,this.paymentId,this.timestampCancel); 277 | } 278 | public String toPrintString(){ 279 | return ""+"id="+this.id+" "+"paymentId="+this.paymentId+" "+"timestampCancel="+this.timestampCancel+" "; 280 | } 281 | public static WHERE whereId() { 282 | return new WHERE("`id`"); 283 | } 284 | public static WHERE wherePaymentId() { 285 | return new WHERE("`paymentId`"); 286 | } 287 | public static WHERE whereTimestampCancel() { 288 | return new WHERE("`timestampCancel`"); 289 | } 290 | public static class WHERE { 291 | /** 292 | * Remember to prepend WHERE on the final SQL statement. 293 | * This is not done by this class due to performance reasons.

294 | *

295 | * Note that it excepts the generated SQL string to be used by a {@link java.sql.PreparedStatement} 296 | * to protect against SQL-Injection.

297 | *

298 | * Also note that the SQL query gets optimized by the database automatically, 299 | * thus It's recommended to make queries as readable as possible and 300 | * not worry that much about performance. 301 | */ 302 | public StringBuilder sqlBuilder = new StringBuilder(); 303 | public StringBuilder orderByBuilder = new StringBuilder(); 304 | public StringBuilder limitBuilder = new StringBuilder(); 305 | List whereObjects = new ArrayList<>(); 306 | private final String columnName; 307 | public WHERE(String columnName) { 308 | this.columnName = columnName; 309 | } 310 | 311 | /** 312 | * Executes the generated SQL statement 313 | * and returns a list of objects matching the query. 314 | */ 315 | public List get() { 316 | String where = sqlBuilder.toString(); 317 | if(!where.isEmpty()) where = " WHERE " + where; 318 | String orderBy = orderByBuilder.toString(); 319 | if(!orderBy.isEmpty()) orderBy = " ORDER BY "+orderBy.substring(0, orderBy.length()-2)+" "; 320 | if(!whereObjects.isEmpty()) 321 | return PendingPaymentCancel.get(where+orderBy+limitBuilder.toString(), whereObjects.toArray()); 322 | else 323 | return PendingPaymentCancel.get(where+orderBy+limitBuilder.toString(), (T[]) null); 324 | } 325 | 326 | /** 327 | * Executes the generated SQL statement 328 | * and returns the size of the list of objects matching the query. 329 | */ 330 | public int count() { 331 | String where = sqlBuilder.toString(); 332 | if(!where.isEmpty()) where = " WHERE " + where; 333 | String orderBy = orderByBuilder.toString(); 334 | if(!orderBy.isEmpty()) orderBy = " ORDER BY "+orderBy.substring(0, orderBy.length()-2)+" "; 335 | if(!whereObjects.isEmpty()) 336 | return PendingPaymentCancel.count(where+orderBy+limitBuilder.toString(), whereObjects.toArray()); 337 | else 338 | return PendingPaymentCancel.count(where+orderBy+limitBuilder.toString(), (T[]) null); 339 | } 340 | 341 | /** 342 | * Executes the generated SQL statement 343 | * and removes the objects matching the query. 344 | */ 345 | public void remove() { 346 | String where = sqlBuilder.toString(); 347 | if(!where.isEmpty()) where = " WHERE " + where; 348 | String orderBy = orderByBuilder.toString(); 349 | if(!orderBy.isEmpty()) orderBy = " ORDER BY "+orderBy.substring(0, orderBy.length()-2)+" "; 350 | if(!whereObjects.isEmpty()) 351 | PendingPaymentCancel.remove(where+orderBy+limitBuilder.toString(), whereObjects.toArray()); 352 | else 353 | PendingPaymentCancel.remove(where+orderBy+limitBuilder.toString(), (T[]) null); 354 | } 355 | 356 | /** 357 | * AND (...)
358 | */ 359 | public WHERE and(WHERE where) { 360 | String sql = where.sqlBuilder.toString(); 361 | if(!sql.isEmpty()) { 362 | sqlBuilder.append("AND (").append(sql).append(") "); 363 | whereObjects.addAll(where.whereObjects); 364 | } 365 | orderByBuilder.append(where.orderByBuilder.toString()); 366 | return this; 367 | } 368 | 369 | /** 370 | * OR (...)
371 | */ 372 | public WHERE or(WHERE where) { 373 | String sql = where.sqlBuilder.toString(); 374 | if(!sql.isEmpty()) { 375 | sqlBuilder.append("OR (").append(sql).append(") "); 376 | whereObjects.addAll(where.whereObjects); 377 | } 378 | orderByBuilder.append(where.orderByBuilder.toString()); 379 | return this; 380 | } 381 | 382 | /** 383 | * columnName = ?
384 | */ 385 | public WHERE is(T obj) { 386 | sqlBuilder.append(columnName).append(" = ? "); 387 | whereObjects.add(obj); 388 | return this; 389 | } 390 | 391 | /** 392 | * columnName IN (?,?,...)
393 | * 394 | * @see https://www.w3schools.com/mysql/mysql_in.asp 395 | */ 396 | public WHERE is(T... objects) { 397 | String s = ""; 398 | for (T obj : objects) { 399 | s += "?,"; 400 | whereObjects.add(obj); 401 | } 402 | s = s.substring(0, s.length() - 1); // Remove last , 403 | sqlBuilder.append(columnName).append(" IN (" + s + ") "); 404 | return this; 405 | } 406 | 407 | /** 408 | * columnName <> ?
409 | */ 410 | public WHERE isNot(T obj) { 411 | sqlBuilder.append(columnName).append(" <> ? "); 412 | whereObjects.add(obj); 413 | return this; 414 | } 415 | 416 | /** 417 | * columnName IS NULL
418 | */ 419 | public WHERE isNull() { 420 | sqlBuilder.append(columnName).append(" IS NULL "); 421 | return this; 422 | } 423 | 424 | /** 425 | * columnName IS NOT NULL
426 | */ 427 | public WHERE isNotNull() { 428 | sqlBuilder.append(columnName).append(" IS NOT NULL "); 429 | return this; 430 | } 431 | 432 | /** 433 | * columnName LIKE ?
434 | * 435 | * @see https://www.w3schools.com/mysql/mysql_like.asp 436 | */ 437 | public WHERE like(T obj) { 438 | sqlBuilder.append(columnName).append(" LIKE ? "); 439 | whereObjects.add(obj); 440 | return this; 441 | } 442 | 443 | /** 444 | * columnName NOT LIKE ?
445 | * 446 | * @see https://www.w3schools.com/mysql/mysql_like.asp 447 | */ 448 | public WHERE notLike(T obj) { 449 | sqlBuilder.append(columnName).append(" NOT LIKE ? "); 450 | whereObjects.add(obj); 451 | return this; 452 | } 453 | 454 | /** 455 | * columnName > ?
456 | */ 457 | public WHERE biggerThan(T obj) { 458 | sqlBuilder.append(columnName).append(" > ? "); 459 | whereObjects.add(obj); 460 | return this; 461 | } 462 | 463 | /** 464 | * columnName < ?
465 | */ 466 | public WHERE smallerThan(T obj) { 467 | sqlBuilder.append(columnName).append(" < ? "); 468 | whereObjects.add(obj); 469 | return this; 470 | } 471 | 472 | /** 473 | * columnName >= ?
474 | */ 475 | public WHERE biggerOrEqual(T obj) { 476 | sqlBuilder.append(columnName).append(" >= ? "); 477 | whereObjects.add(obj); 478 | return this; 479 | } 480 | 481 | /** 482 | * columnName <= ?
483 | */ 484 | public WHERE smallerOrEqual(T obj) { 485 | sqlBuilder.append(columnName).append(" <= ? "); 486 | whereObjects.add(obj); 487 | return this; 488 | } 489 | 490 | /** 491 | * columnName BETWEEN ? AND ?
492 | */ 493 | public WHERE between(T obj1, T obj2) { 494 | sqlBuilder.append(columnName).append(" BETWEEN ? AND ? "); 495 | whereObjects.add(obj1); 496 | whereObjects.add(obj2); 497 | return this; 498 | } 499 | 500 | /** 501 | * columnName ASC,
502 | * 503 | * @see https://www.w3schools.com/mysql/mysql_like.asp 504 | */ 505 | public WHERE smallestFirst() { 506 | orderByBuilder.append(columnName + " ASC, "); 507 | return this; 508 | } 509 | 510 | /** 511 | * columnName DESC,
512 | * 513 | * @see https://www.w3schools.com/mysql/mysql_like.asp 514 | */ 515 | public WHERE biggestFirst() { 516 | orderByBuilder.append(columnName + " DESC, "); 517 | return this; 518 | } 519 | 520 | /** 521 | * LIMIT number
522 | * 523 | * @see https://www.w3schools.com/mysql/mysql_limit.asp 524 | */ 525 | public WHERE limit(int num) { 526 | limitBuilder.append("LIMIT ").append(num + " "); 527 | return this; 528 | } 529 | 530 | } 531 | // The code below will not be removed when re-generating this class. 532 | // Additional code start -> 533 | private PendingPaymentCancel(){} 534 | // Additional code end <- 535 | } 536 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/jsqlgen/payhook/PaymentWarning.java: -------------------------------------------------------------------------------- 1 | package com.osiris.jsqlgen.payhook; 2 | 3 | import java.sql.Connection; 4 | import java.sql.PreparedStatement; 5 | import java.sql.ResultSet; 6 | import java.sql.Statement; 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | import java.util.function.Consumer; 10 | 11 | /** 12 | Generated class by jSQL-Gen 13 | that contains static methods for fetching/updating data from the "PaymentWarning" table. 14 | A single object/instance of this class represents a single row in the table 15 | and data can be accessed via its public fields.

16 | Its not recommended to modify this class but it should be OK to add new methods to it. 17 | If modifications are really needed create a pull request directly to jSQL-Gen instead.
18 | NO EXCEPTIONS is enabled which makes it possible to use this methods outside of try/catch blocks because SQL errors will be caught and thrown as runtime exceptions instead.
19 | */ 20 | public class PaymentWarning{ 21 | public static java.util.concurrent.atomic.AtomicInteger idCounter = new java.util.concurrent.atomic.AtomicInteger(0); 22 | static { 23 | try{ 24 | Connection con = Database.getCon(); 25 | try{ 26 | try (Statement s = con.createStatement()) { 27 | s.executeUpdate("CREATE TABLE IF NOT EXISTS `paymentwarning` (`id` INT NOT NULL PRIMARY KEY)"); 28 | try{s.executeUpdate("ALTER TABLE `paymentwarning` ADD COLUMN `paymentId` INT NOT NULL");}catch(Exception ignored){} 29 | s.executeUpdate("ALTER TABLE `paymentwarning` MODIFY COLUMN `paymentId` INT NOT NULL"); 30 | try{s.executeUpdate("ALTER TABLE `paymentwarning` ADD COLUMN `message` TEXT(65532) DEFAULT NULL");}catch(Exception ignored){} 31 | s.executeUpdate("ALTER TABLE `paymentwarning` MODIFY COLUMN `message` TEXT(65532) DEFAULT NULL"); 32 | } 33 | try (PreparedStatement ps = con.prepareStatement("SELECT id FROM `paymentwarning` ORDER BY id DESC LIMIT 1")) { 34 | ResultSet rs = ps.executeQuery(); 35 | if (rs.next()) idCounter.set(rs.getInt(1) + 1); 36 | } 37 | } 38 | catch(Exception e){ throw new RuntimeException(e); } 39 | finally {Database.freeCon(con);} 40 | }catch(Exception e){ 41 | e.printStackTrace(); 42 | System.err.println("Something went really wrong during table (PaymentWarning) initialisation, thus the program will exit!");System.exit(1);} 43 | } 44 | 45 | /** 46 | Use the static create method instead of this constructor, 47 | if you plan to add this object to the database in the future, since 48 | that method fetches and sets/reserves the {@link #id}. 49 | */ 50 | public PaymentWarning (int id, int paymentId){ 51 | this.id = id;this.paymentId = paymentId; 52 | } 53 | /** 54 | Use the static create method instead of this constructor, 55 | if you plan to add this object to the database in the future, since 56 | that method fetches and sets/reserves the {@link #id}. 57 | */ 58 | public PaymentWarning (int id, int paymentId, String message){ 59 | this.id = id;this.paymentId = paymentId;this.message = message; 60 | } 61 | /** 62 | Database field/value. Not null.
63 | */ 64 | public int id; 65 | /** 66 | Database field/value. Not null.
67 | */ 68 | public int paymentId; 69 | /** 70 | Database field/value.
71 | */ 72 | public String message; 73 | /** 74 | Creates and returns an object that can be added to this table. 75 | Increments the id (thread-safe) and sets it for this object (basically reserves a space in the database). 76 | Note that the parameters of this method represent "NOT NULL" fields in the table and thus should not be null. 77 | Also note that this method will NOT add the object to the table. 78 | */ 79 | public static PaymentWarning create( int paymentId) { 80 | int id = idCounter.getAndIncrement(); 81 | PaymentWarning obj = new PaymentWarning(id, paymentId); 82 | return obj; 83 | } 84 | 85 | /** 86 | Creates and returns an object that can be added to this table. 87 | Increments the id (thread-safe) and sets it for this object (basically reserves a space in the database). 88 | Note that this method will NOT add the object to the table. 89 | */ 90 | public static PaymentWarning create( int paymentId, String message) { 91 | int id = idCounter.getAndIncrement(); 92 | PaymentWarning obj = new PaymentWarning(); 93 | obj.id=id; obj.paymentId=paymentId; obj.message=message; 94 | return obj; 95 | } 96 | 97 | /** 98 | Convenience method for creating and directly adding a new object to the table. 99 | Note that the parameters of this method represent "NOT NULL" fields in the table and thus should not be null. 100 | */ 101 | public static PaymentWarning createAndAdd( int paymentId) { 102 | int id = idCounter.getAndIncrement(); 103 | PaymentWarning obj = new PaymentWarning(id, paymentId); 104 | add(obj); 105 | return obj; 106 | } 107 | 108 | /** 109 | Convenience method for creating and directly adding a new object to the table. 110 | */ 111 | public static PaymentWarning createAndAdd( int paymentId, String message) { 112 | int id = idCounter.getAndIncrement(); 113 | PaymentWarning obj = new PaymentWarning(); 114 | obj.id=id; obj.paymentId=paymentId; obj.message=message; 115 | add(obj); 116 | return obj; 117 | } 118 | 119 | /** 120 | @return a list containing all objects in this table. 121 | */ 122 | public static List get() {return get(null);} 123 | /** 124 | @return object with the provided id or null if there is no object with the provided id in this table. 125 | @throws Exception on SQL issues. 126 | */ 127 | public static PaymentWarning get(int id) { 128 | try{ 129 | return get("WHERE id = "+id).get(0); 130 | }catch(IndexOutOfBoundsException ignored){} 131 | catch(Exception e){throw new RuntimeException(e);} 132 | return null; 133 | } 134 | /** 135 | Example:
136 | get("WHERE username=? AND age=?", "Peter", 33);
137 | @param where can be null. Your SQL WHERE statement (with the leading WHERE). 138 | @param whereValues can be null. Your SQL WHERE statement values to set for '?'. 139 | @return a list containing only objects that match the provided SQL WHERE statement (no matches = empty list). 140 | if that statement is null, returns all the contents of this table. 141 | */ 142 | public static List get(String where, Object... whereValues) { 143 | String sql = "SELECT `id`,`paymentId`,`message`" + 144 | " FROM `paymentwarning`" + 145 | (where != null ? where : ""); 146 | List list = new ArrayList<>(); 147 | Connection con = Database.getCon(); 148 | try (PreparedStatement ps = con.prepareStatement(sql)) { 149 | if(where!=null && whereValues!=null) 150 | for (int i = 0; i < whereValues.length; i++) { 151 | Object val = whereValues[i]; 152 | ps.setObject(i+1, val); 153 | } 154 | ResultSet rs = ps.executeQuery(); 155 | while (rs.next()) { 156 | PaymentWarning obj = new PaymentWarning(); 157 | list.add(obj); 158 | obj.id = rs.getInt(1); 159 | obj.paymentId = rs.getInt(2); 160 | obj.message = rs.getString(3); 161 | } 162 | }catch(Exception e){throw new RuntimeException(e);} 163 | finally{Database.freeCon(con);} 164 | return list; 165 | } 166 | 167 | /** 168 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 169 | */ 170 | public static void getLazy(Consumer> onResultReceived){ 171 | getLazy(onResultReceived, null, 500, null); 172 | } 173 | /** 174 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 175 | */ 176 | public static void getLazy(Consumer> onResultReceived, int limit){ 177 | getLazy(onResultReceived, null, limit, null); 178 | } 179 | /** 180 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 181 | */ 182 | public static void getLazy(Consumer> onResultReceived, Consumer onFinish){ 183 | getLazy(onResultReceived, onFinish, 500, null); 184 | } 185 | /** 186 | * See {@link #getLazy(Consumer, Consumer, int, WHERE)} for details. 187 | */ 188 | public static void getLazy(Consumer> onResultReceived, Consumer onFinish, int limit){ 189 | getLazy(onResultReceived, onFinish, limit, null); 190 | } 191 | /** 192 | * Loads results lazily in a new thread.
193 | * Add {@link Thread#sleep(long)} at the end of your onResultReceived code, to sleep between fetches. 194 | * @param onResultReceived can NOT be null. Gets executed until there are no results left, thus the results list is never empty. 195 | * @param onFinish can be null. Gets executed when finished receiving all results. Provides the total amount of received elements as parameter. 196 | * @param limit the maximum amount of elements for each fetch. 197 | * @param where can be null. This WHERE is not allowed to contain LIMIT and should not contain order by id. 198 | */ 199 | public static void getLazy(Consumer> onResultReceived, Consumer onFinish, int limit, WHERE where) { 200 | new Thread(() -> { 201 | WHERE finalWhere; 202 | if(where == null) finalWhere = new WHERE(""); 203 | else finalWhere = where; 204 | List results; 205 | int lastId = -1; 206 | long count = 0; 207 | while(true){ 208 | results = whereId().biggerThan(lastId).and(finalWhere).limit(limit).get(); 209 | if(results.isEmpty()) break; 210 | lastId = results.get(results.size() - 1).id; 211 | count += results.size(); 212 | onResultReceived.accept(results); 213 | } 214 | if(onFinish!=null) onFinish.accept(count); 215 | }).start(); 216 | } 217 | 218 | public static int count(){ return count(null, null); } 219 | 220 | public static int count(String where, Object... whereValues) { 221 | String sql = "SELECT COUNT(`id`) AS recordCount FROM `paymentwarning`" + 222 | (where != null ? where : ""); 223 | Connection con = Database.getCon(); 224 | try (PreparedStatement ps = con.prepareStatement(sql)) { 225 | if(where!=null && whereValues!=null) 226 | for (int i = 0; i < whereValues.length; i++) { 227 | Object val = whereValues[i]; 228 | ps.setObject(i+1, val); 229 | } 230 | ResultSet rs = ps.executeQuery(); 231 | if (rs.next()) return rs.getInt("recordCount"); 232 | }catch(Exception e){throw new RuntimeException(e);} 233 | finally {Database.freeCon(con);} 234 | return 0; 235 | } 236 | 237 | /** 238 | Searches the provided object in the database (by its id), 239 | and updates all its fields. 240 | @throws Exception when failed to find by id or other SQL issues. 241 | */ 242 | public static void update(PaymentWarning obj) { 243 | String sql = "UPDATE `paymentwarning` SET `id`=?,`paymentId`=?,`message`=? WHERE id="+obj.id; 244 | Connection con = Database.getCon(); 245 | try (PreparedStatement ps = con.prepareStatement(sql)) { 246 | ps.setInt(1, obj.id); 247 | ps.setInt(2, obj.paymentId); 248 | ps.setString(3, obj.message); 249 | ps.executeUpdate(); 250 | }catch(Exception e){throw new RuntimeException(e);} 251 | finally{Database.freeCon(con);} 252 | } 253 | 254 | /** 255 | Adds the provided object to the database (note that the id is not checked for duplicates). 256 | */ 257 | public static void add(PaymentWarning obj) { 258 | String sql = "INSERT INTO `paymentwarning` (`id`,`paymentId`,`message`) VALUES (?,?,?)"; 259 | Connection con = Database.getCon(); 260 | try (PreparedStatement ps = con.prepareStatement(sql)) { 261 | ps.setInt(1, obj.id); 262 | ps.setInt(2, obj.paymentId); 263 | ps.setString(3, obj.message); 264 | ps.executeUpdate(); 265 | }catch(Exception e){throw new RuntimeException(e);} 266 | finally{Database.freeCon(con);} 267 | } 268 | 269 | /** 270 | Deletes the provided object from the database. 271 | */ 272 | public static void remove(PaymentWarning obj) { 273 | remove("WHERE id = "+obj.id); 274 | } 275 | /** 276 | Example:
277 | remove("WHERE username=?", "Peter");
278 | Deletes the objects that are found by the provided SQL WHERE statement, from the database. 279 | @param where can NOT be null. 280 | @param whereValues can be null. Your SQL WHERE statement values to set for '?'. 281 | */ 282 | public static void remove(String where, Object... whereValues) { 283 | java.util.Objects.requireNonNull(where); 284 | String sql = "DELETE FROM `paymentwarning` "+where; 285 | Connection con = Database.getCon(); 286 | try (PreparedStatement ps = con.prepareStatement(sql)) { 287 | if(whereValues != null) 288 | for (int i = 0; i < whereValues.length; i++) { 289 | Object val = whereValues[i]; 290 | ps.setObject(i+1, val); 291 | } 292 | ps.executeUpdate(); 293 | }catch(Exception e){throw new RuntimeException(e);} 294 | finally{Database.freeCon(con);} 295 | } 296 | 297 | public static void removeAll() { 298 | String sql = "DELETE FROM `paymentwarning`"; 299 | Connection con = Database.getCon(); 300 | try (PreparedStatement ps = con.prepareStatement(sql)) { 301 | ps.executeUpdate(); 302 | }catch(Exception e){throw new RuntimeException(e);} 303 | finally{Database.freeCon(con);} 304 | } 305 | 306 | public PaymentWarning clone(){ 307 | return new PaymentWarning(this.id,this.paymentId,this.message); 308 | } 309 | public String toPrintString(){ 310 | return ""+"id="+this.id+" "+"paymentId="+this.paymentId+" "+"message="+this.message+" "; 311 | } 312 | public static WHERE whereId() { 313 | return new WHERE("`id`"); 314 | } 315 | public static WHERE wherePaymentId() { 316 | return new WHERE("`paymentId`"); 317 | } 318 | public static WHERE whereMessage() { 319 | return new WHERE("`message`"); 320 | } 321 | public static class WHERE { 322 | /** 323 | * Remember to prepend WHERE on the final SQL statement. 324 | * This is not done by this class due to performance reasons.

325 | *

326 | * Note that it excepts the generated SQL string to be used by a {@link java.sql.PreparedStatement} 327 | * to protect against SQL-Injection.

328 | *

329 | * Also note that the SQL query gets optimized by the database automatically, 330 | * thus It's recommended to make queries as readable as possible and 331 | * not worry that much about performance. 332 | */ 333 | public StringBuilder sqlBuilder = new StringBuilder(); 334 | public StringBuilder orderByBuilder = new StringBuilder(); 335 | public StringBuilder limitBuilder = new StringBuilder(); 336 | List whereObjects = new ArrayList<>(); 337 | private final String columnName; 338 | public WHERE(String columnName) { 339 | this.columnName = columnName; 340 | } 341 | 342 | /** 343 | * Executes the generated SQL statement 344 | * and returns a list of objects matching the query. 345 | */ 346 | public List get() { 347 | String where = sqlBuilder.toString(); 348 | if(!where.isEmpty()) where = " WHERE " + where; 349 | String orderBy = orderByBuilder.toString(); 350 | if(!orderBy.isEmpty()) orderBy = " ORDER BY "+orderBy.substring(0, orderBy.length()-2)+" "; 351 | if(!whereObjects.isEmpty()) 352 | return PaymentWarning.get(where+orderBy+limitBuilder.toString(), whereObjects.toArray()); 353 | else 354 | return PaymentWarning.get(where+orderBy+limitBuilder.toString(), (T[]) null); 355 | } 356 | 357 | /** 358 | * Executes the generated SQL statement 359 | * and returns the size of the list of objects matching the query. 360 | */ 361 | public int count() { 362 | String where = sqlBuilder.toString(); 363 | if(!where.isEmpty()) where = " WHERE " + where; 364 | String orderBy = orderByBuilder.toString(); 365 | if(!orderBy.isEmpty()) orderBy = " ORDER BY "+orderBy.substring(0, orderBy.length()-2)+" "; 366 | if(!whereObjects.isEmpty()) 367 | return PaymentWarning.count(where+orderBy+limitBuilder.toString(), whereObjects.toArray()); 368 | else 369 | return PaymentWarning.count(where+orderBy+limitBuilder.toString(), (T[]) null); 370 | } 371 | 372 | /** 373 | * Executes the generated SQL statement 374 | * and removes the objects matching the query. 375 | */ 376 | public void remove() { 377 | String where = sqlBuilder.toString(); 378 | if(!where.isEmpty()) where = " WHERE " + where; 379 | String orderBy = orderByBuilder.toString(); 380 | if(!orderBy.isEmpty()) orderBy = " ORDER BY "+orderBy.substring(0, orderBy.length()-2)+" "; 381 | if(!whereObjects.isEmpty()) 382 | PaymentWarning.remove(where+orderBy+limitBuilder.toString(), whereObjects.toArray()); 383 | else 384 | PaymentWarning.remove(where+orderBy+limitBuilder.toString(), (T[]) null); 385 | } 386 | 387 | /** 388 | * AND (...)
389 | */ 390 | public WHERE and(WHERE where) { 391 | String sql = where.sqlBuilder.toString(); 392 | if(!sql.isEmpty()) { 393 | sqlBuilder.append("AND (").append(sql).append(") "); 394 | whereObjects.addAll(where.whereObjects); 395 | } 396 | orderByBuilder.append(where.orderByBuilder.toString()); 397 | return this; 398 | } 399 | 400 | /** 401 | * OR (...)
402 | */ 403 | public WHERE or(WHERE where) { 404 | String sql = where.sqlBuilder.toString(); 405 | if(!sql.isEmpty()) { 406 | sqlBuilder.append("OR (").append(sql).append(") "); 407 | whereObjects.addAll(where.whereObjects); 408 | } 409 | orderByBuilder.append(where.orderByBuilder.toString()); 410 | return this; 411 | } 412 | 413 | /** 414 | * columnName = ?
415 | */ 416 | public WHERE is(T obj) { 417 | sqlBuilder.append(columnName).append(" = ? "); 418 | whereObjects.add(obj); 419 | return this; 420 | } 421 | 422 | /** 423 | * columnName IN (?,?,...)
424 | * 425 | * @see https://www.w3schools.com/mysql/mysql_in.asp 426 | */ 427 | public WHERE is(T... objects) { 428 | String s = ""; 429 | for (T obj : objects) { 430 | s += "?,"; 431 | whereObjects.add(obj); 432 | } 433 | s = s.substring(0, s.length() - 1); // Remove last , 434 | sqlBuilder.append(columnName).append(" IN (" + s + ") "); 435 | return this; 436 | } 437 | 438 | /** 439 | * columnName <> ?
440 | */ 441 | public WHERE isNot(T obj) { 442 | sqlBuilder.append(columnName).append(" <> ? "); 443 | whereObjects.add(obj); 444 | return this; 445 | } 446 | 447 | /** 448 | * columnName IS NULL
449 | */ 450 | public WHERE isNull() { 451 | sqlBuilder.append(columnName).append(" IS NULL "); 452 | return this; 453 | } 454 | 455 | /** 456 | * columnName IS NOT NULL
457 | */ 458 | public WHERE isNotNull() { 459 | sqlBuilder.append(columnName).append(" IS NOT NULL "); 460 | return this; 461 | } 462 | 463 | /** 464 | * columnName LIKE ?
465 | * 466 | * @see https://www.w3schools.com/mysql/mysql_like.asp 467 | */ 468 | public WHERE like(T obj) { 469 | sqlBuilder.append(columnName).append(" LIKE ? "); 470 | whereObjects.add(obj); 471 | return this; 472 | } 473 | 474 | /** 475 | * columnName NOT LIKE ?
476 | * 477 | * @see https://www.w3schools.com/mysql/mysql_like.asp 478 | */ 479 | public WHERE notLike(T obj) { 480 | sqlBuilder.append(columnName).append(" NOT LIKE ? "); 481 | whereObjects.add(obj); 482 | return this; 483 | } 484 | 485 | /** 486 | * columnName > ?
487 | */ 488 | public WHERE biggerThan(T obj) { 489 | sqlBuilder.append(columnName).append(" > ? "); 490 | whereObjects.add(obj); 491 | return this; 492 | } 493 | 494 | /** 495 | * columnName < ?
496 | */ 497 | public WHERE smallerThan(T obj) { 498 | sqlBuilder.append(columnName).append(" < ? "); 499 | whereObjects.add(obj); 500 | return this; 501 | } 502 | 503 | /** 504 | * columnName >= ?
505 | */ 506 | public WHERE biggerOrEqual(T obj) { 507 | sqlBuilder.append(columnName).append(" >= ? "); 508 | whereObjects.add(obj); 509 | return this; 510 | } 511 | 512 | /** 513 | * columnName <= ?
514 | */ 515 | public WHERE smallerOrEqual(T obj) { 516 | sqlBuilder.append(columnName).append(" <= ? "); 517 | whereObjects.add(obj); 518 | return this; 519 | } 520 | 521 | /** 522 | * columnName BETWEEN ? AND ?
523 | */ 524 | public WHERE between(T obj1, T obj2) { 525 | sqlBuilder.append(columnName).append(" BETWEEN ? AND ? "); 526 | whereObjects.add(obj1); 527 | whereObjects.add(obj2); 528 | return this; 529 | } 530 | 531 | /** 532 | * columnName ASC,
533 | * 534 | * @see https://www.w3schools.com/mysql/mysql_like.asp 535 | */ 536 | public WHERE smallestFirst() { 537 | orderByBuilder.append(columnName + " ASC, "); 538 | return this; 539 | } 540 | 541 | /** 542 | * columnName DESC,
543 | * 544 | * @see https://www.w3schools.com/mysql/mysql_like.asp 545 | */ 546 | public WHERE biggestFirst() { 547 | orderByBuilder.append(columnName + " DESC, "); 548 | return this; 549 | } 550 | 551 | /** 552 | * LIMIT number
553 | * 554 | * @see https://www.w3schools.com/mysql/mysql_limit.asp 555 | */ 556 | public WHERE limit(int num) { 557 | limitBuilder.append("LIMIT ").append(num + " "); 558 | return this; 559 | } 560 | 561 | } 562 | // The code below will not be removed when re-generating this class. 563 | // Additional code start -> 564 | private PaymentWarning(){} 565 | // Additional code end <- 566 | } 567 | -------------------------------------------------------------------------------- /src/main/java/com/osiris/payhook/paypal/PayPalUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Osiris Team 3 | * All rights reserved. 4 | * 5 | * This software is copyrighted work licensed under the terms of the 6 | * AutoPlug License. Please consult the file "LICENSE" for details. 7 | */ 8 | 9 | package com.osiris.payhook.paypal; 10 | 11 | 12 | import com.google.gson.JsonArray; 13 | import com.google.gson.JsonElement; 14 | import com.google.gson.JsonObject; 15 | import com.google.gson.JsonParser; 16 | import com.osiris.jlib.json.exceptions.HttpErrorException; 17 | import com.osiris.jlib.json.exceptions.WrongJsonTypeException; 18 | import com.osiris.jsqlgen.payhook.Product; 19 | import com.osiris.payhook.exceptions.ParseBodyException; 20 | import com.osiris.payhook.exceptions.ParseHeaderException; 21 | import com.osiris.payhook.utils.Converter; 22 | import com.paypal.api.payments.Currency; 23 | import com.paypal.base.codec.binary.Base64; 24 | import com.paypal.base.rest.PayPalRESTException; 25 | import com.paypal.core.PayPalHttpClient; 26 | import com.paypal.http.HttpResponse; 27 | import com.paypal.orders.Order; 28 | import com.paypal.orders.OrderRequest; 29 | import com.paypal.orders.OrdersCaptureRequest; 30 | import com.paypal.payments.*; 31 | 32 | import java.io.IOException; 33 | import java.text.DateFormat; 34 | import java.text.ParseException; 35 | import java.text.SimpleDateFormat; 36 | import java.util.*; 37 | 38 | /** 39 | * PayPals' Java SDKs don't cover the complete REST API.
40 | * This class aims to close those gaps.
41 | */ 42 | public class PayPalUtils { 43 | public static String BASE_V1_URL; 44 | public static String LIVE_V1_SANDBOX_BASE_URL = "https://api-m.sandbox.paypal.com/v1"; 45 | public static String LIVE_V1_LIVE_BASE_URL = "https://api-m.paypal.com/v1"; 46 | private final String clientId; 47 | private final String clientSecret; 48 | private final Mode mode; 49 | private final UtilsPayPal utils = new UtilsPayPal(); 50 | private final UtilsPayPalJson utilsJson = new UtilsPayPalJson(); 51 | private String credBase64 = ""; 52 | 53 | public PayPalUtils(String clientId, String clientSecret, Mode mode) { 54 | this.clientId = clientId; 55 | this.clientSecret = clientSecret; 56 | credBase64 = Base64.encodeBase64String((clientId + ":" + clientSecret).getBytes()); 57 | this.mode = mode; 58 | if (mode == Mode.LIVE) { 59 | BASE_V1_URL = LIVE_V1_LIVE_BASE_URL; 60 | } else { 61 | BASE_V1_URL = LIVE_V1_SANDBOX_BASE_URL; 62 | } 63 | } 64 | 65 | public PayPalPlan getPlanById(String planId) throws WrongJsonTypeException, IOException, HttpErrorException { 66 | JsonObject obj = utilsJson.getJsonObject(BASE_V1_URL + "/billing/plans/" + planId, this); 67 | String desc = ""; 68 | if (obj.get("description") != null) 69 | desc = obj.get("description").getAsString(); 70 | String prodId = null; 71 | if (obj.get("product_id") != null) 72 | prodId = obj.get("product_id").getAsString(); 73 | PayPalPlan.Status status = null; 74 | if (obj.get("status") != null) 75 | utils.getPlanStatus(obj.get("status").getAsString()); 76 | else if (obj.get("state") != null) 77 | utils.getPlanStatus(obj.get("state").getAsString()); 78 | return new PayPalPlan( 79 | this, 80 | planId, 81 | prodId, 82 | obj.get("name").getAsString(), 83 | desc, 84 | status); 85 | } 86 | 87 | public PayPalPlan createPlan(Product product, boolean activate) throws IOException, HttpErrorException { 88 | return createPlan(product.paypalProductId, product.name, product.description, product.paymentInterval, 89 | new Converter().toPayPalMoney(product.currency, product.charge), activate); 90 | } 91 | 92 | public PayPalPlan createPlan(String productId, String name, String description, int intervalDays, 93 | Money price, boolean activate) throws IOException, HttpErrorException { 94 | JsonObject obj = new JsonObject(); 95 | obj.addProperty("product_id", productId); 96 | obj.addProperty("name", name); 97 | obj.addProperty("description", description); 98 | JsonArray cycles = new JsonArray(); 99 | obj.add("billing_cycles", cycles); 100 | cycles.add(JsonParser.parseString("{\n" + 101 | " \"frequency\": {\n" + 102 | " \"interval_unit\": \"DAY\",\n" + 103 | " \"interval_count\": " + intervalDays + "\n" + 104 | " },\n" + 105 | " \"tenure_type\": \"REGULAR\",\n" + 106 | " \"sequence\": 1,\n" + // Billing cycle sequence should start with `1` and be consecutive 107 | " \"total_cycles\": 0,\n" + // 0 == INFINITE 108 | " \"pricing_scheme\": {\n" + 109 | " \"fixed_price\": {\n" + 110 | " \"value\": \"" + price.value() + "\",\n" + 111 | " \"currency_code\": \"" + price.currencyCode() + "\"\n" + 112 | " }\n" + 113 | " }\n" + 114 | " }")); 115 | 116 | JsonObject paymentPref = new JsonObject(); 117 | obj.add("payment_preferences", paymentPref); 118 | paymentPref.addProperty("auto_bill_outstanding", "true"); 119 | // TODO setup_fee and taxes support. See https://developer.paypal.com/docs/api/subscriptions/v1/#plans_create 120 | 121 | JsonObject objResponse = 122 | utilsJson.postJsonAndGetResponse(BASE_V1_URL + "/billing/plans", obj, this, 201).getAsJsonObject(); 123 | 124 | PayPalPlan plan = new PayPalPlan(this, objResponse.get("id").getAsString(), productId, name, description, 125 | utils.getPlanStatus(objResponse.get("status").getAsString())); 126 | if (activate && plan.getStatus() != PayPalPlan.Status.ACTIVE) 127 | activatePlan(plan.getPlanId()); 128 | return plan; 129 | } 130 | 131 | public void activatePlan(String planId) throws IOException, HttpErrorException { 132 | utilsJson.postJsonAndGetResponse(BASE_V1_URL + "/billing/plans/" + planId + "/activate", null, this, 204); 133 | } 134 | 135 | /** 136 | * Creates a new product at PayPal with the provided {@link Product}s details. 137 | */ 138 | public JsonObject createProduct(Product product) throws IOException, HttpErrorException { 139 | JsonObject obj = new JsonObject(); 140 | obj.addProperty("name", product.name); 141 | obj.addProperty("description", product.description); 142 | if (product.isRecurring()) 143 | obj.addProperty("type", "SERVICE"); 144 | else 145 | obj.addProperty("type", "DIGITAL"); // TODO Add support for physical goods. 146 | return utilsJson.postJsonAndGetResponse(BASE_V1_URL + "/catalogs/products", obj, this, 201).getAsJsonObject(); 147 | } 148 | 149 | /** 150 | * Updates the product at PayPal with the provided {@link Product}s details. 151 | * 152 | * @throws NullPointerException when {@link Product#paypalProductId} is null. 153 | */ 154 | public PayPalUtils updateProduct(Product product) throws IOException, HttpErrorException { 155 | Objects.requireNonNull(product.paypalProductId); 156 | JsonArray arr = new JsonArray(); 157 | JsonObject patchName = new JsonObject(); 158 | patchName.addProperty("op", "replace"); 159 | patchName.addProperty("path", "/name"); 160 | patchName.addProperty("value", product.name); 161 | arr.add(patchName); 162 | JsonObject patchDesc = new JsonObject(); 163 | patchDesc.addProperty("op", "replace"); 164 | patchDesc.addProperty("path", "/description"); 165 | patchDesc.addProperty("value", product.description); 166 | arr.add(patchDesc); 167 | utilsJson.patchJsonAndGetResponse(BASE_V1_URL + "/catalogs/products/" + product.paypalProductId, arr, this, 204); 168 | return this; 169 | } 170 | 171 | /** 172 | * Returns a string array like this:
173 | * [subscriptionId, approveUrl] 174 | */ 175 | public String[] createSubscription(String brandName, String planId, String customId, String returnUrl, String cancelUrl) throws WrongJsonTypeException, IOException, HttpErrorException, PayPalRESTException { 176 | JsonObject obj = new JsonObject(); 177 | obj.addProperty("plan_id", planId); 178 | obj.addProperty("custom_id", customId); 179 | SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 180 | sf.setTimeZone(TimeZone.getTimeZone("Etc/UTC")); 181 | obj.addProperty("start_time", sf.format(new Date(System.currentTimeMillis() + 60000))); 182 | obj.addProperty("quantity", "1"); 183 | 184 | JsonObject applicationContext = new JsonObject(); 185 | obj.add("application_context", applicationContext); 186 | applicationContext.addProperty("brand_name", brandName); 187 | applicationContext.addProperty("locale", "en-US"); 188 | applicationContext.addProperty("shipping_preference", "NO_SHIPPING"); 189 | applicationContext.addProperty("user_action", "SUBSCRIBE_NOW"); 190 | 191 | JsonObject paymentMethod = new JsonObject(); 192 | applicationContext.add("payment_method", paymentMethod); 193 | paymentMethod.addProperty("payer_selected", "PAYPAL"); 194 | paymentMethod.addProperty("payee_preferred", "IMMEDIATE_PAYMENT_REQUIRED"); 195 | 196 | applicationContext.addProperty("return_url", returnUrl); 197 | applicationContext.addProperty("cancel_url", cancelUrl); 198 | 199 | 200 | JsonObject resultObj = utilsJson.postJsonAndGetResponse(BASE_V1_URL + "/billing/subscriptions", obj, this, 201) 201 | .getAsJsonObject(); 202 | 203 | String approveUrl = null; 204 | for (JsonElement element : 205 | resultObj.get("links").getAsJsonArray()) { 206 | if (element.getAsJsonObject().get("rel").getAsString().equals("approve")) 207 | approveUrl = element.getAsJsonObject().get("href").getAsString(); 208 | } 209 | Objects.requireNonNull(approveUrl); 210 | return new String[]{resultObj.get("id").getAsString(), approveUrl}; 211 | } 212 | 213 | /** 214 | * Can only be done in the first 14 days, thus the first transaction is fetched and a refund for that is tried. 215 | */ 216 | public PayPalUtils refundSubscription(PayPalHttpClient client, String subscriptionId, long subscriptionStart, 217 | Money amount, String note) throws IOException, HttpErrorException { 218 | JsonArray transactions = getSubscriptionTransactions(subscriptionId, subscriptionStart); 219 | refundPayment(client, transactions.get(0).getAsJsonObject().get("id").getAsString(), // transactionId 220 | amount, note); 221 | return this; 222 | } 223 | 224 | public PayPalUtils cancelSubscription(String paypalSubscriptionId) throws IOException, HttpErrorException { 225 | String status = getSubscriptionDetails(paypalSubscriptionId).get("status").getAsString(); 226 | if (status.equalsIgnoreCase("active") || status.equalsIgnoreCase("suspended")) { 227 | JsonObject obj = new JsonObject(); 228 | obj.addProperty("reason", "No reason provided."); 229 | utilsJson.postJsonAndGetResponse(BASE_V1_URL + "/billing/subscriptions/" + paypalSubscriptionId + "/cancel", 230 | obj, this, 204); 231 | } 232 | return this; 233 | } 234 | 235 | public HttpResponse refundPayment(PayPalHttpClient client, String captureOrTransactionId, Money amount, String note) throws IOException { 236 | Objects.requireNonNull(client); 237 | Objects.requireNonNull(captureOrTransactionId); 238 | Objects.requireNonNull(amount); 239 | RefundRequest refundRequest = new RefundRequest(); 240 | refundRequest.amount(amount); 241 | refundRequest.noteToPayer(note); 242 | CapturesRefundRequest request = new CapturesRefundRequest(captureOrTransactionId); 243 | request.prefer("return=representation"); 244 | request.requestBody(refundRequest); 245 | return client.execute(request); 246 | } 247 | 248 | /** 249 | * Since the subscription id is not directly returned in 250 | * webhook event (for example when a payment is made on a subscription), this method can be pretty useful. 251 | * 252 | * @return subscription id or null if not found in the transactions of the last 30 days. 253 | */ 254 | public String findSubscriptionId(String transactionId) throws WrongJsonTypeException, IOException, HttpErrorException { 255 | Objects.requireNonNull(transactionId); 256 | String subscriptionId = null; 257 | JsonArray arr = getTransactionsLast30Days(transactionId); 258 | for (JsonElement el : arr) { 259 | JsonObject transactionInfo = el.getAsJsonObject().getAsJsonObject("transaction_info"); 260 | if (transactionInfo.get("paypal_reference_id_type") != null && transactionInfo.get("paypal_reference_id_type").getAsString().equals("SUB")) { 261 | subscriptionId = transactionInfo.get("paypal_reference_id").getAsString(); 262 | break; 263 | } 264 | } 265 | return subscriptionId; 266 | } 267 | 268 | public JsonArray getTransactionsLast30Days(String transactionId) throws WrongJsonTypeException, IOException, HttpErrorException { 269 | Date endTime = new Date(System.currentTimeMillis()); 270 | Date startTime = new Date(System.currentTimeMillis() - (30L * 24 * 3600000)); // 30 days as milliseconds 271 | String pattern = "yyyy-MM-dd'T'HH:mm:ss'-0000'"; 272 | DateFormat df = new SimpleDateFormat(pattern); 273 | df.setTimeZone(TimeZone.getTimeZone("Etc/UTC")); 274 | String formattedEndTime = df.format(endTime); 275 | String formattedStartTime = df.format(startTime); 276 | return utilsJson.getJsonObject(BASE_V1_URL + "/reporting/transactions" + 277 | "?start_date=" + formattedStartTime + 278 | "&end_date=" + formattedEndTime + 279 | "&transaction_id=" + transactionId + 280 | "&fields=all" + 281 | "&page_size=100" + 282 | "&page=1", this) 283 | .getAsJsonObject().getAsJsonArray("transaction_details"); 284 | } 285 | 286 | /** 287 | * Client id and secret separated by : encoded with Base64. 288 | */ 289 | public String getCredBase64() { 290 | return credBase64; 291 | } 292 | 293 | public String getClientId() { 294 | return clientId; 295 | } 296 | 297 | public String getClientSecret() { 298 | return clientSecret; 299 | } 300 | 301 | public Mode getMode() { 302 | return mode; 303 | } 304 | 305 | public HttpResponse captureOrder(PayPalHttpClient client, String orderId) throws Exception { 306 | OrdersCaptureRequest request = new OrdersCaptureRequest(orderId); 307 | OrderRequest orderRequest = new OrderRequest(); 308 | request.requestBody(orderRequest); 309 | HttpResponse response = client.execute(request); 310 | if (response.statusCode() != 201) { 311 | throw new Exception("Error-Code: " + response.statusCode() + " Status-Message: " + response.result().status()); 312 | } 313 | return response; 314 | } 315 | 316 | public HttpResponse capturePayment(PayPalHttpClient client, String paymentId) throws Exception { 317 | AuthorizationsCaptureRequest request = new AuthorizationsCaptureRequest(paymentId); 318 | CaptureRequest details = new CaptureRequest(); 319 | request.requestBody(details); 320 | HttpResponse response = client.execute(request); 321 | if (response.statusCode() != 201) { 322 | throw new Exception("Error-Code: " + response.statusCode() + " Status-Message: " + response.result().status()); 323 | } 324 | return response; 325 | } 326 | 327 | /** 328 | * @return the transaction-id, aka capture-id. 329 | * @throws Exception if something went wrong with the API request, or if the returned 330 | * http status code is not 200/201, or if the currency code and paid balance don't 331 | * match the expected amount. 332 | */ 333 | public String captureOrder(PayPalHttpClient paypalV2, String orderId, Currency outstandingBalance) throws Exception { 334 | Objects.requireNonNull(orderId); 335 | Objects.requireNonNull(outstandingBalance); 336 | 337 | Order order = null; 338 | OrdersCaptureRequest request = new OrdersCaptureRequest(orderId); 339 | HttpResponse response = paypalV2.execute(request); 340 | order = response.result(); 341 | 342 | String currencyCode = order.purchaseUnits().get(0).payments().captures().get(0).amount().currencyCode(); 343 | String paidBalance = order.purchaseUnits().get(0).payments().captures().get(0).amount().value(); 344 | if (!currencyCode.equals(outstandingBalance.getCurrency())) 345 | throw new Exception("Expected '" + outstandingBalance.getCurrency() + "' currency code, but got '" + currencyCode + "' in the capture!"); 346 | if (!paidBalance.equals(outstandingBalance.getValue())) 347 | throw new Exception("Expected '" + outstandingBalance.getValue() + "' paid balance, but got '" + paidBalance + "' in the capture!"); 348 | 349 | return order.purchaseUnits().get(0).payments().captures().get(0).id(); 350 | } 351 | 352 | public JsonObject captureSubscription(String subscriptionId, com.paypal.orders.Money moneyPaid) throws IOException, HttpErrorException { 353 | JsonObject obj = new JsonObject(); 354 | obj.addProperty("note", "Capture of initial subscription payment."); 355 | obj.addProperty("capture_type", "OUTSTANDING_BALANCE"); 356 | JsonObject amount = new JsonObject(); 357 | obj.add("amount", amount); 358 | amount.addProperty("currency_code", moneyPaid.currencyCode()); 359 | amount.addProperty("value", moneyPaid.value()); 360 | return utilsJson 361 | .postJsonAndGetResponse(BASE_V1_URL + "/billing/subscriptions/" + subscriptionId + "/capture", obj, this) 362 | .getAsJsonObject(); 363 | } 364 | 365 | public JsonObject getSubscriptionDetails(String subscriptionId) throws IOException, HttpErrorException { 366 | return utilsJson 367 | .getJsonElement(BASE_V1_URL + "/billing/subscriptions/" + subscriptionId, this) 368 | .getAsJsonObject(); 369 | } 370 | 371 | public JsonArray getSubscriptionTransactions(String subscriptionId, long subscriptionStart) throws IOException, HttpErrorException { 372 | Date endTime = new Date(System.currentTimeMillis()); 373 | Date startTime = new Date(subscriptionStart - (24 * 3600000)); // Minus 24h to make sure there is no UTC local time interfering 374 | String pattern = "yyyy-MM-dd'T'HH:mm:ss'.000Z'"; 375 | DateFormat df = new SimpleDateFormat(pattern); 376 | String formattedEndTime = df.format(endTime); 377 | String formattedStartTime = df.format(startTime); 378 | JsonElement response = utilsJson 379 | .getJsonElement(BASE_V1_URL + "/billing/subscriptions/" + subscriptionId 380 | + "/transactions?start_time=" + formattedStartTime + "&end_time=" + formattedEndTime, this); 381 | return response.getAsJsonObject().get("transactions").getAsJsonArray(); 382 | } 383 | 384 | public Date getLastPaymentDate(String subscriptionId) throws IOException, HttpErrorException, ParseException, WrongJsonTypeException { 385 | JsonObject obj = new UtilsPayPalJson().getJsonObject( 386 | BASE_V1_URL + "/billing/subscriptions/" + subscriptionId, this) 387 | .getAsJsonObject(); 388 | String timestamp = obj.getAsJsonObject("billing_info").getAsJsonObject("last_payment").get("time").getAsString(); 389 | SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"); 390 | sf.setTimeZone(TimeZone.getTimeZone("Etc/UTC")); 391 | return sf.parse(timestamp); 392 | } 393 | 394 | public JsonArray getWebhooks() throws IOException, HttpErrorException, WrongJsonTypeException { 395 | return utilsJson.getJsonObject( 396 | BASE_V1_URL + "/notifications/webhooks", this) 397 | .getAsJsonObject().getAsJsonArray("webhooks"); 398 | } 399 | 400 | public PayPalUtils createWebhook(String webhookUrl, List eventTypes) throws IOException, HttpErrorException { 401 | return createWebhook(webhookUrl, eventTypes.toArray(new String[0])); 402 | } 403 | 404 | public PayPalUtils createWebhook(String webhookUrl, String... eventTypes) throws IOException, HttpErrorException { 405 | JsonObject obj = new JsonObject(); 406 | obj.addProperty("url", webhookUrl); 407 | JsonArray arr = new JsonArray(); 408 | for (String eventType : 409 | eventTypes) { 410 | JsonObject o = new JsonObject(); 411 | o.addProperty("name", eventType); 412 | arr.add(o); 413 | } 414 | obj.add("event_types", arr); 415 | utilsJson.postJsonAndGetResponse( 416 | BASE_V1_URL + "/notifications/webhooks", obj, this, 201); 417 | return this; 418 | } 419 | 420 | public JsonObject getBalances() throws IOException, HttpErrorException, WrongJsonTypeException { 421 | return utilsJson.getJsonObject( 422 | BASE_V1_URL + "/reporting/balances", this); 423 | } 424 | 425 | public void deleteWebhook(String webhookId) throws IOException, HttpErrorException { 426 | utilsJson.deleteAndGetResponse(BASE_V1_URL + "/notifications/webhooks/" + webhookId, this); 427 | } 428 | 429 | public boolean isWebhookEventValid(String validWebhookId, List validTypesList, Map header, String body) 430 | throws ParseHeaderException, ParseBodyException, IOException, HttpErrorException { 431 | return isWebhookEventValid(new PaypalWebhookEvent(validWebhookId, validTypesList, header, body)); 432 | } 433 | 434 | /** 435 | * Checks if the provided webhook event is valid via the PayPal-REST-API.
436 | * Also sets {@link PaypalWebhookEvent#isValid()} accordingly. 437 | */ 438 | public boolean isWebhookEventValid(PaypalWebhookEvent event) 439 | throws IOException, HttpErrorException, ParseBodyException { 440 | // Check if the webhook types match 441 | List validEventTypes = event.getValidTypesList(); 442 | 443 | // event_type can be either an json array or a normal field. Do stuff accordingly. 444 | JsonElement elementEventType = event.getBody().get("event_type"); 445 | if (elementEventType == null) 446 | elementEventType = event.getBody().get("event_types"); // Check for event_types 447 | if (elementEventType == null) 448 | throw new ParseBodyException("Failed to find key 'event_type' or 'event_types' in the provided json body."); // if the element is still null 449 | 450 | if (elementEventType.isJsonArray()) { 451 | // This means we have multiple event_type objects in the array 452 | JsonArray arrayEventType = elementEventType.getAsJsonArray(); 453 | for (JsonElement singleElementEventType : 454 | arrayEventType) { 455 | JsonObject o = singleElementEventType.getAsJsonObject(); 456 | if (!validEventTypes.contains(o.get("name").getAsString())) { 457 | //throw new WebHookValidationException("No valid type(" + o.get("name") + ") found in the valid types list: " + validEventTypes); 458 | return false; 459 | } 460 | } 461 | } else { 462 | // This means we only have one event_type in the json and not an array. 463 | String webHookType = event.getBody().get("event_type").getAsString(); 464 | if (!validEventTypes.contains(webHookType)) { 465 | //throw new WebHookValidationException("No valid type(" + webHookType + ") found in the valid types list: " + validEventTypes); 466 | return false; 467 | } 468 | 469 | } 470 | 471 | PaypalWebhookEventHeader header = event.getHeader(); 472 | JsonObject json = new JsonObject(); 473 | json.addProperty("transmission_id", header.getTransmissionId()); 474 | json.addProperty("transmission_time", header.getTimestamp()); 475 | json.addProperty("cert_url", header.getCertUrl()); 476 | json.addProperty("auth_algo", header.getAuthAlgorithm()); 477 | json.addProperty("transmission_sig", header.getTransmissionSignature()); 478 | json.addProperty("webhook_id", header.getWebhookId()); 479 | json.add("webhook_event", event.getBody()); 480 | event.setValid(utilsJson.postJsonAndGetResponse(BASE_V1_URL + "/notifications/verify-webhook-signature", json, this) 481 | .getAsJsonObject().get("verification_status").getAsString().equalsIgnoreCase("SUCCESS")); 482 | return event.isValid(); 483 | } 484 | 485 | public enum Mode { 486 | LIVE, SANDBOX 487 | } 488 | } 489 | --------------------------------------------------------------------------------