diff --git a/src/main/java/com/twikey/DocumentGateway.java b/src/main/java/com/twikey/DocumentGateway.java index 5e4e592..466ee78 100644 --- a/src/main/java/com/twikey/DocumentGateway.java +++ b/src/main/java/com/twikey/DocumentGateway.java @@ -42,7 +42,7 @@ protected DocumentGateway(TwikeyClient twikeyClient) { *
  • requireValidation Always start with the registration page, even with all known mandate details No boolean
  • * * - * @param ct Template to use can be found @ https://www.twikey.com/r/admin#/c/template + * @param ct Template to use can be found @ https://www.twikey.com/r/admin#/c/template * @param customer Customer details * @param mandateDetails Map containing any of the parameters in the above table * @throws IOException When no connection could be made @@ -53,20 +53,7 @@ public JSONObject create(long ct, Customer customer, Map mandate Map params = new HashMap<>(mandateDetails); params.put("ct", String.valueOf(ct)); if (customer != null) { - params.put("customerNumber", customer.getNumber()); - params.put("email", customer.getEmail()); - params.put("firstname", customer.getFirstname()); - params.put("lastname", customer.getLastname()); - params.put("l", customer.getLang()); - params.put("address", customer.getStreet()); - params.put("city", customer.getCity()); - params.put("zip", customer.getZip()); - params.put("country", customer.getCountry()); - params.put("mobile", customer.getMobile()); - if(customer.getCompanyName() != null){ - params.put("companyName", customer.getCompanyName()); - params.put("coc", customer.getCoc()); - } + params.putAll(customer.asFormParameters()); } URL myurl = twikeyClient.getUrl("/invite"); diff --git a/src/main/java/com/twikey/RefundGateway.java b/src/main/java/com/twikey/RefundGateway.java new file mode 100644 index 0000000..29b3398 --- /dev/null +++ b/src/main/java/com/twikey/RefundGateway.java @@ -0,0 +1,170 @@ +package com.twikey; + +import com.twikey.callback.RefundCallback; +import com.twikey.modal.Account; +import com.twikey.modal.Customer; +import org.json.JSONArray; +import org.json.JSONObject; +import org.json.JSONTokener; + +import java.io.BufferedReader; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.HashMap; +import java.util.Map; + +import static com.twikey.TwikeyClient.getPostDataString; +public class RefundGateway { + + private final TwikeyClient twikeyClient; + + protected RefundGateway(TwikeyClient twikeyClient) { + this.twikeyClient = twikeyClient; + } + + /** + * Creation of a refund provided the customer was created and has a customerNumber + * @param customerNumber required + * @param transactionDetails required + * + * @return json object containing
    {
    +     *             "id": "11DD32CA20180412220109485",
    +     *             "iban": "BE68068097250734",
    +     *             "bic": "JVBABE22",
    +     *             "amount": 12,
    +     *             "msg": "test",
    +     *             "place": null,
    +     *             "ref": "123",
    +     *             "date": "2018-04-12"
    +     *         }
    + * @throws IOException When no connection could be made + * @throws com.twikey.TwikeyClient.UserException When Twikey returns a user error (400) + */ + public JSONObject create(String customerNumber, Map transactionDetails) throws IOException, TwikeyClient.UserException { + Map params = new HashMap<>(transactionDetails); + params.put("customerNumber", customerNumber); + + URL myurl = twikeyClient.getUrl("/transfer"); + HttpURLConnection con = (HttpURLConnection) myurl.openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + con.setRequestProperty("User-Agent", twikeyClient.getUserAgent()); + con.setRequestProperty("Authorization", twikeyClient.getSessionToken()); + con.setDoOutput(true); + con.setDoInput(true); + + try (DataOutputStream output = new DataOutputStream(con.getOutputStream())) { + output.writeBytes(getPostDataString(params)); + output.flush(); + } + + int responseCode = con.getResponseCode(); + if (responseCode == 200) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()))) { + return new JSONObject(new JSONTokener(br)).getJSONArray("Entries").optJSONObject(0); + } + } else { + String apiError = con.getHeaderField("ApiError"); + throw new TwikeyClient.UserException(apiError); + } + } + + /** + * Creation a beneficiary account (with accompanied customer) + * @param customer required + * @param account required + + * @return json object containing
    {
    +     *     "name": "Beneficiary Name",
    +     *     "iban": "BE68068897250734",
    +     *     "bic": "JVBABE22",
    +     *     "available": true,
    +     *     "address": {
    +     *         "street": "Veldstraat 11",
    +     *         "city": "Gent",
    +     *         "zip": "9000",
    +     *         "country": "BE"
    +     *     }
    +     * }
    + * @throws IOException When no connection could be made + * @throws com.twikey.TwikeyClient.UserException When Twikey returns a user error (400) + */ + public JSONObject createBeneficiaryAccount(Customer customer, Account account) throws IOException, TwikeyClient.UserException { + Map params = new HashMap<>(customer.asFormParameters()); + params.put("iban",account.getIban()); + params.put("bic",account.getBic()); + + URL myurl = twikeyClient.getUrl("/transfers/beneficiaries"); + HttpURLConnection con = (HttpURLConnection) myurl.openConnection(); + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + con.setRequestProperty("User-Agent", twikeyClient.getUserAgent()); + con.setRequestProperty("Authorization", twikeyClient.getSessionToken()); + con.setDoOutput(true); + con.setDoInput(true); + + try (DataOutputStream output = new DataOutputStream(con.getOutputStream())) { + output.writeBytes(getPostDataString(params)); + output.flush(); + } + + int responseCode = con.getResponseCode(); + if (responseCode == 200) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()))) { + return new JSONObject(new JSONTokener(br)); + } + } else { + String apiError = con.getHeaderField("ApiError"); + throw new TwikeyClient.UserException(apiError); + } + } + + /** + * Get updates about all paid refunds + * + * @param callback Callback for every payment + * @param sideloads items to include in the sideloading @link www.twikey.com/api/#transaction-feed + * @throws IOException When a network issue happened + * @throws TwikeyClient.UserException When there was an issue while retrieving the mandates (eg. invalid apikey) + */ + public void feed(RefundCallback callback, String... sideloads) throws IOException, TwikeyClient.UserException { + URL myurl = twikeyClient.getUrl("/transfer",sideloads); + boolean isEmpty; + do{ + HttpURLConnection con = (HttpURLConnection) myurl.openConnection(); + con.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); + con.setRequestProperty("User-Agent", twikeyClient.getUserAgent()); + con.setRequestProperty("Authorization", twikeyClient.getSessionToken()); + + int responseCode = con.getResponseCode(); + if (responseCode == 200) { + try (BufferedReader br = new BufferedReader(new InputStreamReader(con.getInputStream()))) { + JSONObject json = new JSONObject(new JSONTokener(br)); + + JSONArray messagesArr = json.getJSONArray("Entries"); + isEmpty = messagesArr.isEmpty(); + if (!isEmpty) { + for (int i = 0; i < messagesArr.length(); i++) { + JSONObject obj = messagesArr.getJSONObject(i); + callback.refund(obj); + } + } + } + } else { + String apiError = con.getHeaderField("ApiError"); + throw new TwikeyClient.UserException(apiError); + } + } while (!isEmpty); + } +} diff --git a/src/main/java/com/twikey/TwikeyClient.java b/src/main/java/com/twikey/TwikeyClient.java index d98e76f..1100a33 100644 --- a/src/main/java/com/twikey/TwikeyClient.java +++ b/src/main/java/com/twikey/TwikeyClient.java @@ -6,22 +6,22 @@ import javax.crypto.spec.SecretKeySpec; import java.io.DataOutputStream; import java.io.IOException; -import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URL; import java.net.URLEncoder; -import java.nio.charset.StandardCharsets; import java.security.GeneralSecurityException; import java.security.MessageDigest; import java.util.Map; +import static java.nio.charset.StandardCharsets.UTF_8; + /** * Eg. usage or see unittests for more info * *
    - * String apiKey = "87DA7055C5D18DC5F3FC084F9F208AB335340977"; // found in https://www.twikey.com/r/admin#/c/settings/api
    - * long ct = 1420; // found @ https://www.twikey.com/r/admin#/c/template
    + * String apiKey = "87DA7055C5D18DC5F3FC084F9F208AB335340977"; // found in https://www.twikey.com/r/admin#/c/settings/api
    + * long ct = 1420; // found @ https://www.twikey.com/r/admin#/c/template
      * TwikeyAPI api = new TwikeyAPI(apiKey);
      * System.out.println(api.invoice().create(ct,customer,invoiceDetails));
      * System.out.println(api.document().create(ct,null,Map.of()));
    @@ -29,8 +29,6 @@
      */
     public class TwikeyClient {
     
    -    private static final String UTF_8 = "UTF-8";
    -
         private static final String DEFAULT_USER_HEADER = "twikey/java-v0.1.0";
         private static final String PROD_ENVIRONMENT = "https://api.twikey.com/creditor";
         private static final String TEST_ENVIRONMENT = "https://api.beta.twikey.com/creditor";
    @@ -50,6 +48,7 @@ public class TwikeyClient {
         private final InvoiceGateway invoiceGateway;
         private final TransactionGateway transactionGateway;
         private final PaylinkGateway paylinkGateway;
    +    private final RefundGateway refundGateway;
     
         /**
          * @param apikey API key
    @@ -61,6 +60,7 @@ public TwikeyClient(String apikey) {
             this.invoiceGateway = new InvoiceGateway(this);
             this.transactionGateway = new TransactionGateway(this);
             this.paylinkGateway = new PaylinkGateway(this);
    +        this.refundGateway = new RefundGateway(this);
         }
     
         public TwikeyClient withUserAgent(String userAgent) {
    @@ -119,24 +119,19 @@ protected String getSessionToken() throws IOException, UnauthenticatedException
         }
     
         protected static String getPostDataString(Map params) {
    -        try {
    -            StringBuilder result = new StringBuilder();
    -            boolean first = true;
    -            for (Map.Entry entry : params.entrySet()) {
    -                if (first)
    -                    first = false;
    -                else
    -                    result.append("&");
    -
    -                result.append(URLEncoder.encode(entry.getKey(), UTF_8));
    -                result.append("=");
    -                result.append(URLEncoder.encode(entry.getValue(), UTF_8));
    -            }
    -            return result.toString();
    -        } catch (UnsupportedEncodingException e) {
    -            // should not happen on UTF8
    -            throw new RuntimeException(e);
    +        StringBuilder result = new StringBuilder();
    +        boolean first = true;
    +        for (Map.Entry entry : params.entrySet()) {
    +            if (first)
    +                first = false;
    +            else
    +                result.append("&");
    +
    +            result.append(URLEncoder.encode(entry.getKey(), UTF_8));
    +            result.append("=");
    +            result.append(URLEncoder.encode(entry.getValue(), UTF_8));
             }
    +        return result.toString();
         }
     
         public URL getUrl(String path) throws MalformedURLException {
    @@ -177,6 +172,10 @@ public PaylinkGateway paylink() {
             return paylinkGateway;
         }
     
    +    public RefundGateway refund() {
    +        return refundGateway;
    +    }
    +
         public static class UserException extends Throwable {
             public UserException(String apiError) {
                 super(apiError);
    @@ -210,9 +209,9 @@ public boolean verifyWebHookSignature(String signatureHeader, String queryString
             Mac mac;
             try {
                 mac = Mac.getInstance("HmacSHA256");
    -            SecretKeySpec secret = new SecretKeySpec(apiKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
    +            SecretKeySpec secret = new SecretKeySpec(apiKey.getBytes(UTF_8), "HmacSHA256");
                 mac.init(secret);
    -            byte[] calculated = mac.doFinal(queryString.getBytes(StandardCharsets.UTF_8));
    +            byte[] calculated = mac.doFinal(queryString.getBytes(UTF_8));
                 boolean equal = true;
                 for (int i = 0; i < calculated.length; i++) {
                     equal = equal && (providedSignature[i] == calculated[i]);
    @@ -238,13 +237,13 @@ public static boolean verifyExiturlSignature(String websitekey, String document,
             Mac mac;
             try {
                 mac = Mac.getInstance("HmacSHA256");
    -            SecretKeySpec secret = new SecretKeySpec(websitekey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
    +            SecretKeySpec secret = new SecretKeySpec(websitekey.getBytes(UTF_8), "HmacSHA256");
                 mac.init(secret);
                 String payload = document + "/" + status;
                 if (token != null) {
                     payload += "/" + token;
                 }
    -            byte[] calculated = mac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
    +            byte[] calculated = mac.doFinal(payload.getBytes(UTF_8));
                 boolean equal = true;
                 for (int i = 0; i < calculated.length; i++) {
                     equal = equal && (providedSignature[i] == calculated[i]);
    @@ -264,11 +263,11 @@ public static boolean verifyExiturlSignature(String websitekey, String document,
         public static String[] decryptAccountInformation(String websitekey, String document, String encryptedAccount) {
             String key = document + websitekey;
             try {
    -            byte[] keyBytes = MessageDigest.getInstance("MD5").digest(key.getBytes(StandardCharsets.UTF_8));
    +            byte[] keyBytes = MessageDigest.getInstance("MD5").digest(key.getBytes(UTF_8));
                 Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
                 cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBytes, "AES"), new IvParameterSpec(keyBytes));
                 byte[] val = cipher.doFinal(hexStringToByteArray(encryptedAccount));
    -            return new String(val, StandardCharsets.UTF_8).split("/");
    +            return new String(val, UTF_8).split("/");
             } catch (Exception e) {
                 throw new RuntimeException("Exception decrypting : " + encryptedAccount, e);
             }
    @@ -285,7 +284,7 @@ private static long generateOtp(String salt, String privateKey) throws GeneralSe
             byte[] key = hexStringToByteArray(privateKey);
     
             if (salt != null) {
    -            byte[] saltBytes = salt.getBytes(StandardCharsets.UTF_8);
    +            byte[] saltBytes = salt.getBytes(UTF_8);
                 byte[] key2 = new byte[saltBytes.length + key.length];
                 System.arraycopy(saltBytes, 0, key2, 0, saltBytes.length);
                 System.arraycopy(key, 0, key2, saltBytes.length, key.length);
    diff --git a/src/main/java/com/twikey/callback/RefundCallback.java b/src/main/java/com/twikey/callback/RefundCallback.java
    new file mode 100644
    index 0000000..394ebd6
    --- /dev/null
    +++ b/src/main/java/com/twikey/callback/RefundCallback.java
    @@ -0,0 +1,23 @@
    +package com.twikey.callback;
    +
    +import org.json.JSONObject;
    +
    +public interface RefundCallback {
    +
    +    /**
    +     * @param refund Json object containing
    +     * 
      + *
    • id: Twikey id
    • + *
    • iban: IBAN of the beneficiary
    • + *
    • bic: BIC of the beneficiary
    • + *
    • amount: Amount of the refund
    • + *
    • msg: Message for the beneficiary
    • + *
    • place: Optional place
    • + *
    • ref: Your reference
    • + *
    • date: Date when the transfer was requested
    • + *
    • state: Paid
    • + *
    • bkdate: Date when the transfer was done
    • + *
    + */ + void refund(JSONObject refund); +} diff --git a/src/main/java/com/twikey/modal/Account.java b/src/main/java/com/twikey/modal/Account.java new file mode 100644 index 0000000..90aac31 --- /dev/null +++ b/src/main/java/com/twikey/modal/Account.java @@ -0,0 +1,25 @@ +package com.twikey.modal; + +public class Account { + + private final String iban; + private final String bic; + + /** + * @param iban Iban part of the account (mandatory) + * @param bic Bank Identifier, most of the time Twikey will be able to derive the bic from iban, except when specific + * branches need to be targeted in which case it's recommended to add the bic. + */ + public Account(String iban, String bic) { + this.iban = iban; + this.bic = bic; + } + + public String getIban() { + return iban; + } + + public String getBic() { + return bic; + } +} diff --git a/src/main/java/com/twikey/modal/Customer.java b/src/main/java/com/twikey/modal/Customer.java index 805eb40..b92f4bd 100644 --- a/src/main/java/com/twikey/modal/Customer.java +++ b/src/main/java/com/twikey/modal/Customer.java @@ -1,5 +1,8 @@ package com.twikey.modal; +import java.util.HashMap; +import java.util.Map; + public class Customer { private String lastname; @@ -124,4 +127,23 @@ public String getCompanyName() { public String getCoc() { return coc; } + + public Map asFormParameters(){ + Map params = new HashMap<>(); + params.put("customerNumber", getNumber()); + params.put("email", getEmail()); + params.put("firstname", getFirstname()); + params.put("lastname", getLastname()); + params.put("l", getLang()); + params.put("address", getStreet()); + params.put("city", getCity()); + params.put("zip", getZip()); + params.put("country", getCountry()); + params.put("mobile", getMobile()); + if(getCompanyName() != null){ + params.put("companyName", getCompanyName()); + params.put("coc", getCoc()); + } + return params; + } } diff --git a/src/test/java/com/twikey/RefundGatewayTest.java b/src/test/java/com/twikey/RefundGatewayTest.java new file mode 100644 index 0000000..9a5c8ec --- /dev/null +++ b/src/test/java/com/twikey/RefundGatewayTest.java @@ -0,0 +1,67 @@ +package com.twikey; + +import com.twikey.modal.Account; +import com.twikey.modal.Customer; +import org.json.JSONObject; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Test; + +import java.io.IOException; +import java.util.Map; + +import static org.junit.Assert.*; + +public class RefundGatewayTest { + + private final String apiKey = System.getenv("TWIKEY_API_KEY"); // found in https://www.twikey.com/r/admin#/c/settings/api + + private TwikeyClient api; + + private Customer customer; + + private Account account; + + @Before + public void createCustomer(){ + customer = new Customer() + .setNumber("customerNum123") + .setEmail("no-reply@example.com") + .setFirstname("Twikey") + .setLastname("Support") + .setStreet("Derbystraat 43") + .setCity("Gent") + .setZip("9000") + .setCountry("BE") + .setLang("nl") + .setMobile("32498665995"); + + account = new Account("NL46ABNA8910219718","ABNANL2A"); + + api = new TwikeyClient(apiKey) + .withTestEndpoint() + .withUserAgent("twikey-api-java/junit"); + } + + @Test + public void testCreateBeneficiaryAndRefund() throws IOException, TwikeyClient.UserException { + Assume.assumeTrue("APIKey is set", apiKey != null); + // Add beneficiary account explicitly (if mandates exist for the customer this is optional) + JSONObject beneficiaryResponse = api.refund().createBeneficiaryAccount(customer, account); + assertTrue("Available",beneficiaryResponse.getBoolean("available")); + + JSONObject refundResponse = api.refund().create(customer.getNumber(), Map.of( + "iban", account.getIban(), + "message", "Refund faulty item", + "ref", "My internal reference", + "amount", "10.99" + )); + assertNotNull("Refund id",refundResponse.getString("id")); + + api.refund().feed(refund -> { + assertEquals("Refund was PAID", "PAID", refund.getString("state")); + assertNotNull("Refund has ref", refund.getString("ref")); + assertNotNull("Refund has amount", refund.getBigDecimal("amount")); + }); + } +}