Skip to content

Commit 79b31f5

Browse files
⚠️ Add strongly typed EventNotifications (#2036)
* generate event deliveries * tests almost working [skip ci] * Fix tests * rename thinEvent * fix fetchEvent name * fix typo * fix naming and object * update example * update comment * update comment * fix doc
1 parent ccf0670 commit 79b31f5

17 files changed

+481
-123
lines changed

src/main/java/com/stripe/StripeClient.java

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import com.stripe.exception.SignatureVerificationException;
44
import com.stripe.exception.StripeException;
55
import com.stripe.model.StripeObject;
6-
import com.stripe.model.ThinEvent;
6+
import com.stripe.model.v2.EventNotification;
77
import com.stripe.net.*;
88
import com.stripe.net.Webhook.Signature;
99
import java.net.PasswordAuthentication;
@@ -52,9 +52,9 @@ protected StripeResponseGetter getResponseGetter() {
5252
* @return the StripeEvent instance
5353
* @throws SignatureVerificationException if the verification fails.
5454
*/
55-
public ThinEvent parseThinEvent(String payload, String sigHeader, String secret)
55+
public EventNotification parseEventNotification(String payload, String sigHeader, String secret)
5656
throws SignatureVerificationException {
57-
return parseThinEvent(payload, sigHeader, secret, Webhook.DEFAULT_TOLERANCE);
57+
return parseEventNotification(payload, sigHeader, secret, Webhook.DEFAULT_TOLERANCE);
5858
}
5959

6060
/**
@@ -70,11 +70,12 @@ public ThinEvent parseThinEvent(String payload, String sigHeader, String secret)
7070
* @return the StripeEvent instance
7171
* @throws SignatureVerificationException if the verification fails.
7272
*/
73-
public ThinEvent parseThinEvent(String payload, String sigHeader, String secret, long tolerance)
73+
public EventNotification parseEventNotification(
74+
String payload, String sigHeader, String secret, long tolerance)
7475
throws SignatureVerificationException {
7576
Signature.verifyHeader(payload, sigHeader, secret, tolerance);
7677

77-
return ApiResource.GSON.fromJson(payload, ThinEvent.class);
78+
return EventNotification.fromJson(payload, this);
7879
}
7980

8081
/**

src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEvent.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import com.stripe.exception.StripeException;
66
import com.stripe.model.billing.Meter;
77
import com.stripe.model.v2.Event;
8+
import com.stripe.model.v2.Event.RelatedObject;
89
import java.time.Instant;
910
import java.util.List;
1011
import lombok.Getter;
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// File generated from our OpenAPI spec
2+
package com.stripe.events;
3+
4+
import com.google.gson.annotations.SerializedName;
5+
import com.stripe.exception.StripeException;
6+
import com.stripe.model.billing.Meter;
7+
import com.stripe.model.v2.Event.RelatedObject;
8+
import com.stripe.model.v2.EventNotification;
9+
import lombok.Getter;
10+
11+
@Getter
12+
public final class V1BillingMeterErrorReportTriggeredEventNotification extends EventNotification {
13+
@SerializedName("related_object")
14+
15+
/** Object containing the reference to API resource relevant to the event. */
16+
RelatedObject relatedObject;
17+
18+
/** Retrieves the related object from the API. Make an API request on every call. */
19+
public Meter fetchRelatedObject() throws StripeException {
20+
return (Meter) super.fetchRelatedObject(this.relatedObject);
21+
}
22+
/** Retrieve the corresponding full event from the Stripe API. */
23+
@Override
24+
public V1BillingMeterErrorReportTriggeredEvent fetchEvent() throws StripeException {
25+
return (V1BillingMeterErrorReportTriggeredEvent) super.fetchEvent();
26+
}
27+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// File generated from our OpenAPI spec
2+
package com.stripe.events;
3+
4+
import com.stripe.exception.StripeException;
5+
import com.stripe.model.v2.EventNotification;
6+
7+
public final class V1BillingMeterNoMeterFoundEventNotification extends EventNotification {
8+
/** Retrieve the corresponding full event from the Stripe API. */
9+
@Override
10+
public V1BillingMeterNoMeterFoundEvent fetchEvent() throws StripeException {
11+
return (V1BillingMeterNoMeterFoundEvent) super.fetchEvent();
12+
}
13+
}

src/main/java/com/stripe/events/V2CoreEventDestinationPingEvent.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import com.google.gson.annotations.SerializedName;
55
import com.stripe.exception.StripeException;
66
import com.stripe.model.v2.Event;
7+
import com.stripe.model.v2.Event.RelatedObject;
78
import com.stripe.model.v2.EventDestination;
89
import lombok.Getter;
910

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// File generated from our OpenAPI spec
2+
package com.stripe.events;
3+
4+
import com.google.gson.annotations.SerializedName;
5+
import com.stripe.exception.StripeException;
6+
import com.stripe.model.v2.Event.RelatedObject;
7+
import com.stripe.model.v2.EventDestination;
8+
import com.stripe.model.v2.EventNotification;
9+
import lombok.Getter;
10+
11+
@Getter
12+
public final class V2CoreEventDestinationPingEventNotification extends EventNotification {
13+
@SerializedName("related_object")
14+
15+
/** Object containing the reference to API resource relevant to the event. */
16+
RelatedObject relatedObject;
17+
18+
/** Retrieves the related object from the API. Make an API request on every call. */
19+
public EventDestination fetchRelatedObject() throws StripeException {
20+
return (EventDestination) super.fetchRelatedObject(this.relatedObject);
21+
}
22+
/** Retrieve the corresponding full event from the Stripe API. */
23+
@Override
24+
public V2CoreEventDestinationPingEvent fetchEvent() throws StripeException {
25+
return (V2CoreEventDestinationPingEvent) super.fetchEvent();
26+
}
27+
}

src/main/java/com/stripe/examples/ThinEventWebhookHandler.java renamed to src/main/java/com/stripe/examples/EventNotificationWebhookHandler.java

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,12 @@
22

33
import com.stripe.StripeClient;
44
import com.stripe.events.V1BillingMeterErrorReportTriggeredEvent;
5+
import com.stripe.events.V1BillingMeterErrorReportTriggeredEventNotification;
56
import com.stripe.exception.StripeException;
6-
import com.stripe.model.ThinEvent;
77
import com.stripe.model.billing.Meter;
88
import com.stripe.model.v2.Event;
9+
import com.stripe.model.v2.EventNotification;
10+
import com.stripe.model.v2.UnknownEventNotification;
911
import com.sun.net.httpserver.HttpExchange;
1012
import com.sun.net.httpserver.HttpHandler;
1113
import com.sun.net.httpserver.HttpServer;
@@ -16,18 +18,18 @@
1618
import java.nio.charset.StandardCharsets;
1719

1820
/**
19-
* Receive and process thin events like the v1.billing.meter.error_report_triggered event.
21+
* Receive and process EventNotifications like the v1.billing.meter.error_report_triggered event.
2022
*
2123
* <p>In this example, we:
2224
*
2325
* <ul>
24-
* <li>use parseThinEvent to parse the received thin event webhook body
25-
* <li>call StripeClient.v2.core.events.retrieve to retrieve the flil event object
26+
* <li>use parseEventNotification to parse the received event notification webhook body
27+
* <li>call StripeClient.v2.core.events.retrieve to retrieve the full event object
2628
* <li>if it is a V1BillingMeterErrorReportTriggeredEvent event type, call fetchRelatedObject to
2729
* retrieve the Billing Meter object associated with the event.
2830
* </ul>
2931
*/
30-
public class ThinEventWebhookHandler {
32+
public class EventNotificationWebhookHandler {
3133
private static final String API_KEY = System.getenv("STRIPE_API_KEY");
3234
private static final String WEBHOOK_SECRET = System.getenv("WEBHOOK_SECRET");
3335

@@ -65,20 +67,34 @@ public void handle(HttpExchange exchange) throws IOException {
6567
String sigHeader = exchange.getRequestHeaders().getFirst("Stripe-Signature");
6668

6769
try {
68-
ThinEvent thinEvent = client.parseThinEvent(webhookBody, sigHeader, WEBHOOK_SECRET);
70+
EventNotification eventNotif =
71+
client.parseEventNotification(webhookBody, sigHeader, WEBHOOK_SECRET);
6972

70-
// Fetch the event data to understand the failure
71-
Event baseEvent = client.v2().core().events().retrieve(thinEvent.getId());
72-
if (baseEvent instanceof V1BillingMeterErrorReportTriggeredEvent) {
73-
V1BillingMeterErrorReportTriggeredEvent event =
74-
(V1BillingMeterErrorReportTriggeredEvent) baseEvent;
75-
Meter meter = event.fetchRelatedObject();
73+
// determine what sort of event you have
74+
if (eventNotif instanceof V1BillingMeterErrorReportTriggeredEventNotification) {
75+
V1BillingMeterErrorReportTriggeredEventNotification eventNotification =
76+
(V1BillingMeterErrorReportTriggeredEventNotification) eventNotif;
7677

77-
String meterId = meter.getId();
78-
System.out.println(meterId);
78+
// after casting, can fetch the related object (which is correctly typed)
79+
Meter meter = eventNotification.fetchRelatedObject();
80+
System.out.println(meter.getId());
7981

80-
// Record the failures and alert your team
81-
// Add your logic here
82+
V1BillingMeterErrorReportTriggeredEvent event = eventNotification.fetchEvent();
83+
System.out.println(event.getData().getDeveloperMessageSummary());
84+
85+
// add additional logic
86+
}
87+
// ... check other event types you know about
88+
else if (eventNotif instanceof UnknownEventNotification) {
89+
UnknownEventNotification unknownEvent = (UnknownEventNotification) eventNotif;
90+
System.out.println("Received unknown event: " + unknownEvent.getId());
91+
// can keep matching on the "type" field
92+
// other helper methods still work, but you'll have to handle types yourself
93+
if (unknownEvent.getType().equals("some.new.event")) {
94+
Event event = unknownEvent.fetchEvent();
95+
System.out.println(event.getReason());
96+
// handle
97+
}
8298
}
8399

84100
exchange.sendResponseHeaders(200, -1);

src/main/java/com/stripe/model/ThinEvent.java

Lines changed: 0 additions & 42 deletions
This file was deleted.

src/main/java/com/stripe/model/ThinEventRelatedObject.java

Lines changed: 0 additions & 16 deletions
This file was deleted.
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
package com.stripe.model.v2;
2+
3+
import com.google.gson.JsonObject;
4+
import com.google.gson.annotations.SerializedName;
5+
import com.stripe.StripeClient;
6+
import com.stripe.exception.StripeException;
7+
import com.stripe.model.StripeObject;
8+
import com.stripe.model.v2.Event.RelatedObject;
9+
import com.stripe.net.ApiMode;
10+
import com.stripe.net.ApiResource;
11+
import com.stripe.net.ApiResource.RequestMethod;
12+
import com.stripe.net.RawRequestOptions;
13+
import com.stripe.net.StripeResponse;
14+
import java.time.Instant;
15+
import lombok.AccessLevel;
16+
import lombok.Getter;
17+
18+
/**
19+
* `EventNotification` represents the common properties for json that's delivered from an Event
20+
* Destination. A concrete child of `EventNotification` will be returned from
21+
* `StripeClient.parseEventNotificaion()`. You will likely want to cast that object to a more
22+
* specific child of `EventNotification`, like `PushedV1BillingMeterErrorReportTriggeredEvent`
23+
*/
24+
@Getter
25+
public abstract class EventNotification {
26+
/**
27+
* For more details about Request, please refer to the <a href="https://docs.stripe.com/api">API
28+
* Reference.</a>
29+
*/
30+
@Getter
31+
public static class Request {
32+
/** ID of the API request that caused the event. */
33+
@SerializedName("id")
34+
String id;
35+
36+
/** The idempotency key transmitted during the request. */
37+
@SerializedName("idempotency_key")
38+
String idempotencyKey;
39+
}
40+
41+
@Getter
42+
public static class Reason {
43+
/** Information on the API request that instigated the event. */
44+
@SerializedName("request")
45+
Request request;
46+
47+
/**
48+
* Event reason type.
49+
*
50+
* <p>Equal to {@code request}.
51+
*/
52+
@SerializedName("type")
53+
String type;
54+
}
55+
56+
/** Unique identifier for the event. */
57+
@SerializedName("id")
58+
public String id;
59+
60+
/** The type of the event. */
61+
@SerializedName("type")
62+
public String type;
63+
64+
/** Time at which the object was created. */
65+
@SerializedName("created")
66+
public Instant created;
67+
68+
/** Livemode indicates if the event is from a production(true) or test(false) account. */
69+
@SerializedName("livemode")
70+
public Boolean livemode;
71+
72+
/** [Optional] Authentication context needed to fetch the event or related object. */
73+
@SerializedName("context")
74+
public String context;
75+
76+
/** [Optional] Reason for the event. */
77+
@SerializedName("reason")
78+
public Reason reason;
79+
80+
@Getter(AccessLevel.NONE)
81+
protected transient StripeClient client;
82+
83+
/**
84+
* Helper for constructing an Event Notification. Doesn't perform signature validation, so you
85+
* should use {@link com.stripe.StripeClient#parseEventNotification} instead for initial handling.
86+
* This is useful in unit tests and working with EventNotifications that you've already validated
87+
* the authenticity of.
88+
*/
89+
public static EventNotification fromJson(String payload, StripeClient client) {
90+
// don't love the double json parse here, but I don't think we can avoid it?
91+
JsonObject jsonObject = ApiResource.GSON.fromJson(payload, JsonObject.class).getAsJsonObject();
92+
93+
Class<? extends EventNotification> cls =
94+
EventNotificationClassLookup.eventClassLookup.get(jsonObject.get("type").getAsString());
95+
if (cls == null) {
96+
cls = UnknownEventNotification.class;
97+
}
98+
99+
EventNotification e = ApiResource.GSON.fromJson(payload, cls);
100+
e.client = client;
101+
return e;
102+
}
103+
104+
private RawRequestOptions getRequestOptions() {
105+
if (context == null) {
106+
return null;
107+
}
108+
return new RawRequestOptions.RawRequestOptionsBuilder().setStripeContext(context).build();
109+
}
110+
111+
/* retrieves the full payload for an event. Protected because individual push classes use it, but type it correctly */
112+
protected Event fetchEvent() throws StripeException {
113+
StripeResponse response =
114+
client.rawRequest(
115+
RequestMethod.GET, String.format("/v2/core/events/%s", id), null, getRequestOptions());
116+
117+
return (Event) client.deserialize(response.body(), ApiMode.V2);
118+
}
119+
120+
/** Retrieves the object associated with the event. */
121+
protected StripeObject fetchRelatedObject(RelatedObject relatedObject) throws StripeException {
122+
if (relatedObject == null) {
123+
// used by UnknownEventNotification, so be a little defensive
124+
return null;
125+
}
126+
127+
String relativeUrl = relatedObject.getUrl();
128+
129+
StripeResponse response =
130+
client.rawRequest(RequestMethod.GET, relativeUrl, null, getRequestOptions());
131+
132+
return client.deserialize(response.body(), ApiMode.getMode(relativeUrl));
133+
}
134+
}

0 commit comments

Comments
 (0)