Skip to content

Commit

Permalink
Added refund gateway (fixes #3)
Browse files Browse the repository at this point in the history
  • Loading branch information
koen-serry committed May 1, 2022
1 parent 4e8a95a commit b538249
Show file tree
Hide file tree
Showing 7 changed files with 338 additions and 45 deletions.
17 changes: 2 additions & 15 deletions src/main/java/com/twikey/DocumentGateway.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ protected DocumentGateway(TwikeyClient twikeyClient) {
* <li>requireValidation Always start with the registration page, even with all known mandate details No boolean</li>
* </ul>
*
* @param ct Template to use can be found @ https://www.twikey.com/r/admin#/c/template
* @param ct Template to use can be found @ <a href="https://www.twikey.com/r/admin#/c/template">https://www.twikey.com/r/admin#/c/template</a>
* @param customer Customer details
* @param mandateDetails Map containing any of the parameters in the above table
* @throws IOException When no connection could be made
Expand All @@ -53,20 +53,7 @@ public JSONObject create(long ct, Customer customer, Map<String, String> mandate
Map<String, String> 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");
Expand Down
170 changes: 170 additions & 0 deletions src/main/java/com/twikey/RefundGateway.java
Original file line number Diff line number Diff line change
@@ -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
* <ul>
* <li>customerNumber The customer number</li>
* <li>iban Iban of the beneficiary</li>
* <li>message Message to the creditor Yes string </li>
* <li>amount Amount to be send</li>
* <li>ref Reference of the transaction</li>
* <li>date Required execution date of the transaction (ReqdExctnDt)</li>
* <li>place Optional place</li>
* </ul>
* @return json object containing <pre>{
* "id": "11DD32CA20180412220109485",
* "iban": "BE68068097250734",
* "bic": "JVBABE22",
* "amount": 12,
* "msg": "test",
* "place": null,
* "ref": "123",
* "date": "2018-04-12"
* }</pre>
* @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<String, String> transactionDetails) throws IOException, TwikeyClient.UserException {
Map<String, String> 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 <pre>{
* "name": "Beneficiary Name",
* "iban": "BE68068897250734",
* "bic": "JVBABE22",
* "available": true,
* "address": {
* "street": "Veldstraat 11",
* "city": "Gent",
* "zip": "9000",
* "country": "BE"
* }
* }</pre>
* @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<String, String> 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 <a href="https://www.twikey.com/api/#transaction-feed">www.twikey.com/api/#transaction-feed</a>
* @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);
}
}
59 changes: 29 additions & 30 deletions src/main/java/com/twikey/TwikeyClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,29 @@
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
*
* <pre>
* 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 <a href="https://www.twikey.com/r/admin#/c/settings/api">https://www.twikey.com/r/admin#/c/settings/api</a>
* long ct = 1420; // found @ <a href="https://www.twikey.com/r/admin#/c/template">https://www.twikey.com/r/admin#/c/template</a>
* TwikeyAPI api = new TwikeyAPI(apiKey);
* System.out.println(api.invoice().create(ct,customer,invoiceDetails));
* System.out.println(api.document().create(ct,null,Map.of()));
* </pre>
*/
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";
Expand All @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -119,24 +119,19 @@ protected String getSessionToken() throws IOException, UnauthenticatedException
}

protected static String getPostDataString(Map<String, String> params) {
try {
StringBuilder result = new StringBuilder();
boolean first = true;
for (Map.Entry<String, String> 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<String, String> 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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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]);
Expand All @@ -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]);
Expand All @@ -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);
}
Expand All @@ -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);
Expand Down
23 changes: 23 additions & 0 deletions src/main/java/com/twikey/callback/RefundCallback.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.twikey.callback;

import org.json.JSONObject;

public interface RefundCallback {

/**
* @param refund Json object containing
* <ul>
* <li>id: Twikey id</li>
* <li>iban: IBAN of the beneficiary</li>
* <li>bic: BIC of the beneficiary</li>
* <li>amount: Amount of the refund</li>
* <li>msg: Message for the beneficiary</li>
* <li>place: Optional place</li>
* <li>ref: Your reference</li>
* <li>date: Date when the transfer was requested</li>
* <li>state: Paid</li>
* <li>bkdate: Date when the transfer was done</li>
* </ul>
*/
void refund(JSONObject refund);
}
Loading

0 comments on commit b538249

Please sign in to comment.