Skip to content

Commit 9d2e734

Browse files
committed
Added paymentfeed
1 parent c5071ae commit 9d2e734

5 files changed

Lines changed: 299 additions & 2 deletions

File tree

src/main/java/com/twikey/InvoiceGateway.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.twikey;
22

33
import com.twikey.callback.InvoiceCallback;
4+
import com.twikey.callback.PaymentCallback;
45
import com.twikey.modal.InvoiceRequests;
56
import com.twikey.modal.InvoiceResponse;
67
import com.twikey.modal.ResponseUtils;
@@ -329,6 +330,44 @@ public void feed(InvoiceCallback invoiceCallback, String... sideloads) throws IO
329330
} while (!isEmpty);
330331
}
331332

333+
/**
334+
* Get updates about all payments on invoices (paymentlink, direct debit, card, transfer, ...)
335+
*
336+
* @param paymentCallback Callback for every payment
337+
* @throws IOException When a network issue happened
338+
* @throws TwikeyClient.UserException When there was an issue while retrieving the mandates (eg. invalid apikey)
339+
*/
340+
public void payment(PaymentCallback paymentCallback) throws IOException, TwikeyClient.UserException {
341+
boolean isEmpty;
342+
do {
343+
HttpRequest request = HttpRequest.newBuilder(twikeyClient.getUrl("/invoice/payment/feed"))
344+
.headers("Content-Type", HTTP_FORM_ENCODED)
345+
.headers("User-Agent", twikeyClient.getUserAgent())
346+
.headers("Authorization", twikeyClient.getSessionToken())
347+
.GET()
348+
.build();
349+
HttpResponse<String> response = twikeyClient.send(request, HttpResponse.BodyHandlers.ofString());
350+
int responseCode = response.statusCode();
351+
if (responseCode == 200) {
352+
try {
353+
JSONObject json = new JSONObject(new JSONTokener(response.body()));
354+
355+
JSONArray invoicesArr = json.getJSONArray("Payments");
356+
isEmpty = invoicesArr.isEmpty();
357+
if (!invoicesArr.isEmpty()) {
358+
for (int i = 0; i < invoicesArr.length(); i++) {
359+
JSONObject obj = invoicesArr.getJSONObject(i);
360+
paymentCallback.payment(InvoiceResponse.Event.fromJson(obj));
361+
}
362+
}
363+
} catch (JSONException e) {
364+
throw new RuntimeException(e);
365+
}
366+
} else {
367+
throw new TwikeyClient.UserException(apiError(response));
368+
}
369+
} while (!isEmpty);
370+
}
332371

333372
/**
334373
* See <a href="https://www.twikey.com/api/#retrieve-invoice-pdf">Twikey API - Retrieve Invoice PDF</a>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.twikey.callback;
2+
3+
import com.twikey.modal.InvoiceResponse;
4+
5+
public interface PaymentCallback {
6+
/**
7+
* Callback with one payment event at a time
8+
*
9+
* @param payment event
10+
*/
11+
void payment(InvoiceResponse.Event payment);
12+
}

src/main/java/com/twikey/modal/InvoiceResponse.java

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.json.JSONObject;
66

77
import java.io.InputStream;
8+
import java.time.Instant;
89
import java.util.HashMap;
910
import java.util.Map;
1011

@@ -174,4 +175,114 @@ public String toString() {
174175
*/
175176
record Pdf(InputStream content, String filename) {}
176177

178+
179+
enum EventType {
180+
PAYMENT,
181+
PAYMENT_FAILURE,
182+
REFUND;
183+
184+
public static EventType parse(String eventType) {
185+
if (eventType == null) {
186+
return null;
187+
}
188+
return switch (eventType) {
189+
case "payment" -> PAYMENT;
190+
case "payment_failure" -> PAYMENT_FAILURE;
191+
case "refund" -> REFUND;
192+
default -> null;
193+
};
194+
}
195+
}
196+
197+
enum GatewayType {
198+
BANK,
199+
PSP;
200+
201+
public static GatewayType parse(String type) {
202+
if (type == null) {
203+
return null;
204+
}
205+
return switch (type) {
206+
case "bank" -> BANK;
207+
case "psp" -> PSP;
208+
default -> null;
209+
};
210+
}
211+
}
212+
213+
record Origin(
214+
String object, // "invoice"
215+
String id,
216+
String number,
217+
String ref
218+
) {
219+
public static Origin fromJson(JSONObject origin) {
220+
return new Origin(
221+
origin.getString("object"),
222+
origin.getString("id"),
223+
origin.getString("number"),
224+
origin.optString("ref")
225+
);
226+
}
227+
}
228+
229+
record Gateway(int id, String name, GatewayType type, String iban /* nullable*/) {
230+
public static Gateway fromJson(JSONObject gateway) {
231+
return new Gateway(
232+
gateway.getInt("id"),
233+
gateway.getString("name"),
234+
GatewayType.parse(gateway.getString("type")),
235+
gateway.optString("iban")
236+
);
237+
}
238+
}
239+
240+
record EventError(
241+
String code,
242+
String description,
243+
String category,
244+
String externalCode,
245+
String action,
246+
int actionStep
247+
) {
248+
public static EventError parse(JSONObject json) {
249+
if (json == null) return null;
250+
return new EventError(
251+
json.getString("code"),
252+
json.getString("description"),
253+
json.getString("category"),
254+
json.getString("externalCode"),
255+
json.optString("action"),
256+
json.optInt("actionStep")
257+
);
258+
}
259+
}
260+
261+
record Event(
262+
String eventId,
263+
EventType eventType,
264+
Instant occurredAt,
265+
double amount,
266+
String currency,
267+
Origin origin,
268+
Gateway gateway,
269+
Map<String, Object> details,
270+
EventError error // nullable
271+
) {
272+
public static Event fromJson(JSONObject json) {
273+
Map<String, Object> details = json.getJSONObject("details").toMap();
274+
return
275+
new Event(
276+
json.getString("eventId"),
277+
EventType.parse(json.getString("eventType")),
278+
Instant.parse(json.getString("occurredAt")),
279+
json.getDouble("amount"),
280+
json.getString("currency"),
281+
Origin.fromJson(json.getJSONObject("origin")),
282+
Gateway.fromJson(json.getJSONObject("gateway")),
283+
details,
284+
EventError.parse(json.optJSONObject("error"))
285+
);
286+
}
287+
}
177288
}

src/test/java/com/twikey/InvoiceGatewayTest.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package com.twikey;
22

3-
import com.twikey.callback.InvoiceCallback;
43
import com.twikey.modal.DocumentRequests;
54
import com.twikey.modal.InvoiceRequests;
65
import com.twikey.modal.InvoiceResponse;
@@ -108,7 +107,7 @@ public void testBatchCreation() throws IOException, TwikeyClient.UserException {
108107
@Test
109108
public void getInvoicesAndDetails() throws IOException, TwikeyClient.UserException {
110109
Assume.assumeTrue("APIKey is set", apiKey != null);
111-
api.invoice().feed((InvoiceCallback) updatedInvoice -> {
110+
api.invoice().feed(updatedInvoice -> {
112111
String newState = "";
113112
if (Objects.equals(updatedInvoice.getState(), "PAID")) {
114113
String lastpayment_ = updatedInvoice.getLastpayment();
@@ -157,4 +156,12 @@ public void testRetrieveInvoicePdf() throws IOException, TwikeyClient.UserExcept
157156
invoiceId, pdf.filename(), retrievedBytes.length);
158157
}
159158

159+
@Test
160+
public void getInvoicePayments() throws IOException, TwikeyClient.UserException {
161+
Assume.assumeTrue("APIKey is set", apiKey != null);
162+
api.invoice().payment(payment -> {
163+
assertNotNull("payment", payment);
164+
System.out.printf("Payment event with number %s %s euro %s%n", payment.origin(), payment.amount(), payment.details());
165+
});
166+
}
160167
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package com.twikey.modal;
2+
3+
import org.json.JSONArray;
4+
import org.json.JSONObject;
5+
import org.json.JSONTokener;
6+
import org.junit.Test;
7+
8+
public class InvoiceResponseTest {
9+
@Test
10+
public void testIMport() {
11+
String feed = """
12+
{
13+
"Payments": [
14+
{
15+
"eventId": "evt_mXvrIbQj14D7Z",
16+
"eventType": "payment",
17+
"occurredAt": "2026-01-15T15:19:19.471Z",
18+
"amount": 30,
19+
"currency": "EUR",
20+
"origin": {
21+
"object": "invoice",
22+
"id": "d9e601e7-18ee-4a88-a71c-bc68ace0021e",
23+
"number": "192274614",
24+
"ref": "truearchitect"
25+
},
26+
"gateway": {
27+
"id": 847,
28+
"name": "ABN NL23XXXXXXXXXX8701",
29+
"type": "bank",
30+
"iban": "NL23ABNA0838498701"
31+
},
32+
"details": {
33+
"source": "direct_debit",
34+
"paymentId": 7868235,
35+
"transactionE2e": "7864B9BB0E6440159E430875C6E7809D",
36+
"mndtId": "Y3QQB4VSUTNQ47R"
37+
}
38+
},
39+
{
40+
"eventId": "evt_2wndunjvlJNQP",
41+
"eventType": "payment_failure",
42+
"occurredAt": "2026-01-15T15:19:21.976Z",
43+
"amount": 30,
44+
"currency": "EUR",
45+
"origin": {
46+
"object": "invoice",
47+
"id": "d9e601e7-18ee-4a88-a71c-bc68ace0021e",
48+
"number": "192274614",
49+
"ref": "truearchitect"
50+
},
51+
"gateway": {
52+
"id": 847,
53+
"name": "ABN NL23XXXXXXXXXX8701",
54+
"type": "bank",
55+
"iban": "NL23ABNA0838498701"
56+
},
57+
"details": {
58+
"source": "direct_debit",
59+
"paymentId": 7868235,
60+
"transactionE2e": "7864B9BB0E6440159E430875C6E7809D",
61+
"mndtId": "Y3QQB4VSUTNQ47R"
62+
},
63+
"error": {
64+
"code": "not_routable",
65+
"description": "Bank not reachable",
66+
"category": "other",
67+
"externalCode": "PY01",
68+
"action": "send_payment_link",
69+
"actionStep": 1
70+
}
71+
},
72+
{
73+
"eventId": "evt_vDvDIyeWbPnKr",
74+
"eventType": "payment",
75+
"occurredAt": "2026-01-15T15:20:06.134Z",
76+
"amount": 5,
77+
"currency": "EUR",
78+
"origin": {
79+
"object": "invoice",
80+
"id": "929b5c17-c2ef-4027-b2e2-73e900bcd33f",
81+
"number": "993109187",
82+
"ref": "falsegrow"
83+
},
84+
"gateway": {
85+
"id": 1579,
86+
"name": "Mollie",
87+
"type": "psp",
88+
"iban": null
89+
},
90+
"details": {
91+
"source": "payment_link",
92+
"linkId": 812589,
93+
"linkMethod": "mastercard"
94+
}
95+
},
96+
{
97+
"eventId": "evt_3wJQianYwvJmY",
98+
"eventType": "refund",
99+
"occurredAt": "2026-01-15T15:20:36.755Z",
100+
"amount": 811,
101+
"currency": "EUR",
102+
"origin": {
103+
"object": "invoice",
104+
"id": "550d75bb-7cff-4b8d-92f9-d9d56b6daa9d",
105+
"number": "634326789",
106+
"ref": "trueengineer"
107+
},
108+
"gateway": {
109+
"id": 847,
110+
"name": "ABN NL23XXXXXXXXXX8701",
111+
"type": "bank",
112+
"iban": "NL23ABNA0838498701"
113+
},
114+
"details": {
115+
"source": "credit_transfer",
116+
"customerIban": "NL23INGB7520051579",
117+
"refundE2e": "B78C1AF520260115152026162308603"
118+
}
119+
}
120+
]
121+
}""";
122+
JSONObject json = new JSONObject(new JSONTokener(feed));
123+
JSONArray payments = json.getJSONArray("Payments");
124+
payments.iterator().forEachRemaining(item -> {
125+
InvoiceResponse.Event.fromJson((JSONObject) item);
126+
});
127+
}
128+
}

0 commit comments

Comments
 (0)