From ccf0670ce3690e70b307aa1c81841514d1836d74 Mon Sep 17 00:00:00 2001 From: "stripe-openapi[bot]" <105521251+stripe-openapi[bot]@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:46:59 -0700 Subject: [PATCH 1/2] Update generated code for v2023 and 2025-09-30.clover (#2054) Co-authored-by: Stripe OpenAPI <105521251+stripe-openapi[bot]@users.noreply.github.com> --- .../stripe/model/EventDataClassLookup.java | 155 +++++++++--------- 1 file changed, 80 insertions(+), 75 deletions(-) diff --git a/src/main/java/com/stripe/model/EventDataClassLookup.java b/src/main/java/com/stripe/model/EventDataClassLookup.java index b22584af125..20b214eed72 100644 --- a/src/main/java/com/stripe/model/EventDataClassLookup.java +++ b/src/main/java/com/stripe/model/EventDataClassLookup.java @@ -14,81 +14,86 @@ public final class EventDataClassLookup { public static final Map> classLookup = new HashMap<>(); static { - classLookup.put("account", Account.class); - classLookup.put("account_link", AccountLink.class); - classLookup.put("account_session", AccountSession.class); - classLookup.put("apple_pay_domain", ApplePayDomain.class); - classLookup.put("application", Application.class); - classLookup.put("application_fee", ApplicationFee.class); - classLookup.put("balance", Balance.class); - classLookup.put("balance_settings", BalanceSettings.class); - classLookup.put("balance_transaction", BalanceTransaction.class); - classLookup.put("bank_account", BankAccount.class); - classLookup.put("capability", Capability.class); - classLookup.put("card", Card.class); - classLookup.put("cash_balance", CashBalance.class); - classLookup.put("charge", Charge.class); - classLookup.put("confirmation_token", ConfirmationToken.class); - classLookup.put("connect_collection_transfer", ConnectCollectionTransfer.class); - classLookup.put("country_spec", CountrySpec.class); - classLookup.put("coupon", Coupon.class); - classLookup.put("credit_note", CreditNote.class); - classLookup.put("credit_note_line_item", CreditNoteLineItem.class); - classLookup.put("customer", Customer.class); - classLookup.put("customer_balance_transaction", CustomerBalanceTransaction.class); - classLookup.put("customer_cash_balance_transaction", CustomerCashBalanceTransaction.class); - classLookup.put("customer_session", CustomerSession.class); - classLookup.put("discount", Discount.class); - classLookup.put("dispute", Dispute.class); - classLookup.put("ephemeral_key", EphemeralKey.class); - classLookup.put("event", Event.class); - classLookup.put("exchange_rate", ExchangeRate.class); - classLookup.put("fee_refund", FeeRefund.class); - classLookup.put("file", File.class); - classLookup.put("file_link", FileLink.class); - classLookup.put("funding_instructions", FundingInstructions.class); - classLookup.put("invoice", Invoice.class); - classLookup.put("invoice_payment", InvoicePayment.class); - classLookup.put("invoice_rendering_template", InvoiceRenderingTemplate.class); - classLookup.put("invoiceitem", InvoiceItem.class); - classLookup.put("item", LineItem.class); - classLookup.put("line_item", InvoiceLineItem.class); - classLookup.put("login_link", LoginLink.class); - classLookup.put("mandate", Mandate.class); - classLookup.put("payment_intent", PaymentIntent.class); - classLookup.put("payment_link", PaymentLink.class); - classLookup.put("payment_method", PaymentMethod.class); - classLookup.put("payment_method_configuration", PaymentMethodConfiguration.class); - classLookup.put("payment_method_domain", PaymentMethodDomain.class); - classLookup.put("payout", Payout.class); - classLookup.put("person", Person.class); - classLookup.put("plan", Plan.class); - classLookup.put("price", Price.class); - classLookup.put("product", Product.class); - classLookup.put("product_feature", ProductFeature.class); - classLookup.put("promotion_code", PromotionCode.class); - classLookup.put("quote", Quote.class); - classLookup.put("refund", Refund.class); - classLookup.put("reserve_transaction", ReserveTransaction.class); - classLookup.put("review", Review.class); - classLookup.put("setup_attempt", SetupAttempt.class); - classLookup.put("setup_intent", SetupIntent.class); - classLookup.put("shipping_rate", ShippingRate.class); - classLookup.put("source", Source.class); - classLookup.put("source_mandate_notification", SourceMandateNotification.class); - classLookup.put("source_transaction", SourceTransaction.class); - classLookup.put("subscription", Subscription.class); - classLookup.put("subscription_item", SubscriptionItem.class); - classLookup.put("subscription_schedule", SubscriptionSchedule.class); - classLookup.put("tax_code", TaxCode.class); - classLookup.put("tax_deducted_at_source", TaxDeductedAtSource.class); - classLookup.put("tax_id", TaxId.class); - classLookup.put("tax_rate", TaxRate.class); - classLookup.put("token", Token.class); - classLookup.put("topup", Topup.class); - classLookup.put("transfer", Transfer.class); - classLookup.put("transfer_reversal", TransferReversal.class); - classLookup.put("webhook_endpoint", WebhookEndpoint.class); + classLookup.put("account", com.stripe.model.Account.class); + classLookup.put("account_link", com.stripe.model.AccountLink.class); + classLookup.put("account_session", com.stripe.model.AccountSession.class); + classLookup.put("apple_pay_domain", com.stripe.model.ApplePayDomain.class); + classLookup.put("application", com.stripe.model.Application.class); + classLookup.put("application_fee", com.stripe.model.ApplicationFee.class); + classLookup.put("balance", com.stripe.model.Balance.class); + classLookup.put("balance_settings", com.stripe.model.BalanceSettings.class); + classLookup.put("balance_transaction", com.stripe.model.BalanceTransaction.class); + classLookup.put("bank_account", com.stripe.model.BankAccount.class); + classLookup.put("capability", com.stripe.model.Capability.class); + classLookup.put("card", com.stripe.model.Card.class); + classLookup.put("cash_balance", com.stripe.model.CashBalance.class); + classLookup.put("charge", com.stripe.model.Charge.class); + classLookup.put("confirmation_token", com.stripe.model.ConfirmationToken.class); + classLookup.put( + "connect_collection_transfer", com.stripe.model.ConnectCollectionTransfer.class); + classLookup.put("country_spec", com.stripe.model.CountrySpec.class); + classLookup.put("coupon", com.stripe.model.Coupon.class); + classLookup.put("credit_note", com.stripe.model.CreditNote.class); + classLookup.put("credit_note_line_item", com.stripe.model.CreditNoteLineItem.class); + classLookup.put("customer", com.stripe.model.Customer.class); + classLookup.put( + "customer_balance_transaction", com.stripe.model.CustomerBalanceTransaction.class); + classLookup.put( + "customer_cash_balance_transaction", com.stripe.model.CustomerCashBalanceTransaction.class); + classLookup.put("customer_session", com.stripe.model.CustomerSession.class); + classLookup.put("discount", com.stripe.model.Discount.class); + classLookup.put("dispute", com.stripe.model.Dispute.class); + classLookup.put("ephemeral_key", com.stripe.model.EphemeralKey.class); + classLookup.put("event", com.stripe.model.Event.class); + classLookup.put("exchange_rate", com.stripe.model.ExchangeRate.class); + classLookup.put("fee_refund", com.stripe.model.FeeRefund.class); + classLookup.put("file", com.stripe.model.File.class); + classLookup.put("file_link", com.stripe.model.FileLink.class); + classLookup.put("funding_instructions", com.stripe.model.FundingInstructions.class); + classLookup.put("invoice", com.stripe.model.Invoice.class); + classLookup.put("invoice_payment", com.stripe.model.InvoicePayment.class); + classLookup.put("invoice_rendering_template", com.stripe.model.InvoiceRenderingTemplate.class); + classLookup.put("invoiceitem", com.stripe.model.InvoiceItem.class); + classLookup.put("item", com.stripe.model.LineItem.class); + classLookup.put("line_item", com.stripe.model.InvoiceLineItem.class); + classLookup.put("login_link", com.stripe.model.LoginLink.class); + classLookup.put("mandate", com.stripe.model.Mandate.class); + classLookup.put("payment_intent", com.stripe.model.PaymentIntent.class); + classLookup.put("payment_link", com.stripe.model.PaymentLink.class); + classLookup.put("payment_method", com.stripe.model.PaymentMethod.class); + classLookup.put( + "payment_method_configuration", com.stripe.model.PaymentMethodConfiguration.class); + classLookup.put("payment_method_domain", com.stripe.model.PaymentMethodDomain.class); + classLookup.put("payout", com.stripe.model.Payout.class); + classLookup.put("person", com.stripe.model.Person.class); + classLookup.put("plan", com.stripe.model.Plan.class); + classLookup.put("price", com.stripe.model.Price.class); + classLookup.put("product", com.stripe.model.Product.class); + classLookup.put("product_feature", com.stripe.model.ProductFeature.class); + classLookup.put("promotion_code", com.stripe.model.PromotionCode.class); + classLookup.put("quote", com.stripe.model.Quote.class); + classLookup.put("refund", com.stripe.model.Refund.class); + classLookup.put("reserve_transaction", com.stripe.model.ReserveTransaction.class); + classLookup.put("review", com.stripe.model.Review.class); + classLookup.put("setup_attempt", com.stripe.model.SetupAttempt.class); + classLookup.put("setup_intent", com.stripe.model.SetupIntent.class); + classLookup.put("shipping_rate", com.stripe.model.ShippingRate.class); + classLookup.put("source", com.stripe.model.Source.class); + classLookup.put( + "source_mandate_notification", com.stripe.model.SourceMandateNotification.class); + classLookup.put("source_transaction", com.stripe.model.SourceTransaction.class); + classLookup.put("subscription", com.stripe.model.Subscription.class); + classLookup.put("subscription_item", com.stripe.model.SubscriptionItem.class); + classLookup.put("subscription_schedule", com.stripe.model.SubscriptionSchedule.class); + classLookup.put("tax_code", com.stripe.model.TaxCode.class); + classLookup.put("tax_deducted_at_source", com.stripe.model.TaxDeductedAtSource.class); + classLookup.put("tax_id", com.stripe.model.TaxId.class); + classLookup.put("tax_rate", com.stripe.model.TaxRate.class); + classLookup.put("token", com.stripe.model.Token.class); + classLookup.put("topup", com.stripe.model.Topup.class); + classLookup.put("transfer", com.stripe.model.Transfer.class); + classLookup.put("transfer_reversal", com.stripe.model.TransferReversal.class); + classLookup.put("webhook_endpoint", com.stripe.model.WebhookEndpoint.class); classLookup.put("apps.secret", com.stripe.model.apps.Secret.class); From 79b31f5590f0dc08dbfbd5d3f220e678f3156d1e Mon Sep 17 00:00:00 2001 From: David Brownman <109395161+xavdid-stripe@users.noreply.github.com> Date: Wed, 24 Sep 2025 19:16:10 -0700 Subject: [PATCH 2/2] =?UTF-8?q?=E2=9A=A0=EF=B8=8F=20Add=20strongly=20typed?= =?UTF-8?q?=20EventNotifications=20(#2036)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 --- src/main/java/com/stripe/StripeClient.java | 11 +- ...BillingMeterErrorReportTriggeredEvent.java | 1 + ...ErrorReportTriggeredEventNotification.java | 27 +++ ...ingMeterNoMeterFoundEventNotification.java | 13 ++ .../V2CoreEventDestinationPingEvent.java | 1 + ...EventDestinationPingEventNotification.java | 27 +++ ...a => EventNotificationWebhookHandler.java} | 48 +++-- src/main/java/com/stripe/model/ThinEvent.java | 42 ---- .../stripe/model/ThinEventRelatedObject.java | 16 -- .../stripe/model/v2/EventNotification.java | 134 ++++++++++++ .../v2/EventNotificationClassLookup.java | 28 +++ .../model/v2/UnknownEventNotification.java | 35 +++ src/main/java/com/stripe/net/ApiMode.java | 12 +- src/main/java/com/stripe/net/ApiRequest.java | 2 +- .../java/com/stripe/net/RawApiRequest.java | 2 +- src/test/java/com/stripe/BaseStripeTest.java | 6 +- .../java/com/stripe/StripeClientTest.java | 199 ++++++++++++++---- 17 files changed, 481 insertions(+), 123 deletions(-) create mode 100644 src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEventNotification.java create mode 100644 src/main/java/com/stripe/events/V1BillingMeterNoMeterFoundEventNotification.java create mode 100644 src/main/java/com/stripe/events/V2CoreEventDestinationPingEventNotification.java rename src/main/java/com/stripe/examples/{ThinEventWebhookHandler.java => EventNotificationWebhookHandler.java} (56%) delete mode 100644 src/main/java/com/stripe/model/ThinEvent.java delete mode 100644 src/main/java/com/stripe/model/ThinEventRelatedObject.java create mode 100644 src/main/java/com/stripe/model/v2/EventNotification.java create mode 100644 src/main/java/com/stripe/model/v2/EventNotificationClassLookup.java create mode 100644 src/main/java/com/stripe/model/v2/UnknownEventNotification.java diff --git a/src/main/java/com/stripe/StripeClient.java b/src/main/java/com/stripe/StripeClient.java index 5618cd8b500..d5f2fb79f22 100644 --- a/src/main/java/com/stripe/StripeClient.java +++ b/src/main/java/com/stripe/StripeClient.java @@ -3,7 +3,7 @@ import com.stripe.exception.SignatureVerificationException; import com.stripe.exception.StripeException; import com.stripe.model.StripeObject; -import com.stripe.model.ThinEvent; +import com.stripe.model.v2.EventNotification; import com.stripe.net.*; import com.stripe.net.Webhook.Signature; import java.net.PasswordAuthentication; @@ -52,9 +52,9 @@ protected StripeResponseGetter getResponseGetter() { * @return the StripeEvent instance * @throws SignatureVerificationException if the verification fails. */ - public ThinEvent parseThinEvent(String payload, String sigHeader, String secret) + public EventNotification parseEventNotification(String payload, String sigHeader, String secret) throws SignatureVerificationException { - return parseThinEvent(payload, sigHeader, secret, Webhook.DEFAULT_TOLERANCE); + return parseEventNotification(payload, sigHeader, secret, Webhook.DEFAULT_TOLERANCE); } /** @@ -70,11 +70,12 @@ public ThinEvent parseThinEvent(String payload, String sigHeader, String secret) * @return the StripeEvent instance * @throws SignatureVerificationException if the verification fails. */ - public ThinEvent parseThinEvent(String payload, String sigHeader, String secret, long tolerance) + public EventNotification parseEventNotification( + String payload, String sigHeader, String secret, long tolerance) throws SignatureVerificationException { Signature.verifyHeader(payload, sigHeader, secret, tolerance); - return ApiResource.GSON.fromJson(payload, ThinEvent.class); + return EventNotification.fromJson(payload, this); } /** diff --git a/src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEvent.java b/src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEvent.java index bab476098a3..89ec7c14531 100644 --- a/src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEvent.java +++ b/src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEvent.java @@ -5,6 +5,7 @@ import com.stripe.exception.StripeException; import com.stripe.model.billing.Meter; import com.stripe.model.v2.Event; +import com.stripe.model.v2.Event.RelatedObject; import java.time.Instant; import java.util.List; import lombok.Getter; diff --git a/src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEventNotification.java b/src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEventNotification.java new file mode 100644 index 00000000000..367a95aa50f --- /dev/null +++ b/src/main/java/com/stripe/events/V1BillingMeterErrorReportTriggeredEventNotification.java @@ -0,0 +1,27 @@ +// File generated from our OpenAPI spec +package com.stripe.events; + +import com.google.gson.annotations.SerializedName; +import com.stripe.exception.StripeException; +import com.stripe.model.billing.Meter; +import com.stripe.model.v2.Event.RelatedObject; +import com.stripe.model.v2.EventNotification; +import lombok.Getter; + +@Getter +public final class V1BillingMeterErrorReportTriggeredEventNotification extends EventNotification { + @SerializedName("related_object") + + /** Object containing the reference to API resource relevant to the event. */ + RelatedObject relatedObject; + + /** Retrieves the related object from the API. Make an API request on every call. */ + public Meter fetchRelatedObject() throws StripeException { + return (Meter) super.fetchRelatedObject(this.relatedObject); + } + /** Retrieve the corresponding full event from the Stripe API. */ + @Override + public V1BillingMeterErrorReportTriggeredEvent fetchEvent() throws StripeException { + return (V1BillingMeterErrorReportTriggeredEvent) super.fetchEvent(); + } +} diff --git a/src/main/java/com/stripe/events/V1BillingMeterNoMeterFoundEventNotification.java b/src/main/java/com/stripe/events/V1BillingMeterNoMeterFoundEventNotification.java new file mode 100644 index 00000000000..e533858cff0 --- /dev/null +++ b/src/main/java/com/stripe/events/V1BillingMeterNoMeterFoundEventNotification.java @@ -0,0 +1,13 @@ +// File generated from our OpenAPI spec +package com.stripe.events; + +import com.stripe.exception.StripeException; +import com.stripe.model.v2.EventNotification; + +public final class V1BillingMeterNoMeterFoundEventNotification extends EventNotification { + /** Retrieve the corresponding full event from the Stripe API. */ + @Override + public V1BillingMeterNoMeterFoundEvent fetchEvent() throws StripeException { + return (V1BillingMeterNoMeterFoundEvent) super.fetchEvent(); + } +} diff --git a/src/main/java/com/stripe/events/V2CoreEventDestinationPingEvent.java b/src/main/java/com/stripe/events/V2CoreEventDestinationPingEvent.java index 7d94b5d35f5..f56280dc61d 100644 --- a/src/main/java/com/stripe/events/V2CoreEventDestinationPingEvent.java +++ b/src/main/java/com/stripe/events/V2CoreEventDestinationPingEvent.java @@ -4,6 +4,7 @@ import com.google.gson.annotations.SerializedName; import com.stripe.exception.StripeException; import com.stripe.model.v2.Event; +import com.stripe.model.v2.Event.RelatedObject; import com.stripe.model.v2.EventDestination; import lombok.Getter; diff --git a/src/main/java/com/stripe/events/V2CoreEventDestinationPingEventNotification.java b/src/main/java/com/stripe/events/V2CoreEventDestinationPingEventNotification.java new file mode 100644 index 00000000000..e6d0c7da525 --- /dev/null +++ b/src/main/java/com/stripe/events/V2CoreEventDestinationPingEventNotification.java @@ -0,0 +1,27 @@ +// File generated from our OpenAPI spec +package com.stripe.events; + +import com.google.gson.annotations.SerializedName; +import com.stripe.exception.StripeException; +import com.stripe.model.v2.Event.RelatedObject; +import com.stripe.model.v2.EventDestination; +import com.stripe.model.v2.EventNotification; +import lombok.Getter; + +@Getter +public final class V2CoreEventDestinationPingEventNotification extends EventNotification { + @SerializedName("related_object") + + /** Object containing the reference to API resource relevant to the event. */ + RelatedObject relatedObject; + + /** Retrieves the related object from the API. Make an API request on every call. */ + public EventDestination fetchRelatedObject() throws StripeException { + return (EventDestination) super.fetchRelatedObject(this.relatedObject); + } + /** Retrieve the corresponding full event from the Stripe API. */ + @Override + public V2CoreEventDestinationPingEvent fetchEvent() throws StripeException { + return (V2CoreEventDestinationPingEvent) super.fetchEvent(); + } +} diff --git a/src/main/java/com/stripe/examples/ThinEventWebhookHandler.java b/src/main/java/com/stripe/examples/EventNotificationWebhookHandler.java similarity index 56% rename from src/main/java/com/stripe/examples/ThinEventWebhookHandler.java rename to src/main/java/com/stripe/examples/EventNotificationWebhookHandler.java index f26cdf99bae..d5869d57281 100644 --- a/src/main/java/com/stripe/examples/ThinEventWebhookHandler.java +++ b/src/main/java/com/stripe/examples/EventNotificationWebhookHandler.java @@ -2,10 +2,12 @@ import com.stripe.StripeClient; import com.stripe.events.V1BillingMeterErrorReportTriggeredEvent; +import com.stripe.events.V1BillingMeterErrorReportTriggeredEventNotification; import com.stripe.exception.StripeException; -import com.stripe.model.ThinEvent; import com.stripe.model.billing.Meter; import com.stripe.model.v2.Event; +import com.stripe.model.v2.EventNotification; +import com.stripe.model.v2.UnknownEventNotification; import com.sun.net.httpserver.HttpExchange; import com.sun.net.httpserver.HttpHandler; import com.sun.net.httpserver.HttpServer; @@ -16,18 +18,18 @@ import java.nio.charset.StandardCharsets; /** - * Receive and process thin events like the v1.billing.meter.error_report_triggered event. + * Receive and process EventNotifications like the v1.billing.meter.error_report_triggered event. * *

In this example, we: * *

*/ -public class ThinEventWebhookHandler { +public class EventNotificationWebhookHandler { private static final String API_KEY = System.getenv("STRIPE_API_KEY"); private static final String WEBHOOK_SECRET = System.getenv("WEBHOOK_SECRET"); @@ -65,20 +67,34 @@ public void handle(HttpExchange exchange) throws IOException { String sigHeader = exchange.getRequestHeaders().getFirst("Stripe-Signature"); try { - ThinEvent thinEvent = client.parseThinEvent(webhookBody, sigHeader, WEBHOOK_SECRET); + EventNotification eventNotif = + client.parseEventNotification(webhookBody, sigHeader, WEBHOOK_SECRET); - // Fetch the event data to understand the failure - Event baseEvent = client.v2().core().events().retrieve(thinEvent.getId()); - if (baseEvent instanceof V1BillingMeterErrorReportTriggeredEvent) { - V1BillingMeterErrorReportTriggeredEvent event = - (V1BillingMeterErrorReportTriggeredEvent) baseEvent; - Meter meter = event.fetchRelatedObject(); + // determine what sort of event you have + if (eventNotif instanceof V1BillingMeterErrorReportTriggeredEventNotification) { + V1BillingMeterErrorReportTriggeredEventNotification eventNotification = + (V1BillingMeterErrorReportTriggeredEventNotification) eventNotif; - String meterId = meter.getId(); - System.out.println(meterId); + // after casting, can fetch the related object (which is correctly typed) + Meter meter = eventNotification.fetchRelatedObject(); + System.out.println(meter.getId()); - // Record the failures and alert your team - // Add your logic here + V1BillingMeterErrorReportTriggeredEvent event = eventNotification.fetchEvent(); + System.out.println(event.getData().getDeveloperMessageSummary()); + + // add additional logic + } + // ... check other event types you know about + else if (eventNotif instanceof UnknownEventNotification) { + UnknownEventNotification unknownEvent = (UnknownEventNotification) eventNotif; + System.out.println("Received unknown event: " + unknownEvent.getId()); + // can keep matching on the "type" field + // other helper methods still work, but you'll have to handle types yourself + if (unknownEvent.getType().equals("some.new.event")) { + Event event = unknownEvent.fetchEvent(); + System.out.println(event.getReason()); + // handle + } } exchange.sendResponseHeaders(200, -1); diff --git a/src/main/java/com/stripe/model/ThinEvent.java b/src/main/java/com/stripe/model/ThinEvent.java deleted file mode 100644 index 5b7bd129c17..00000000000 --- a/src/main/java/com/stripe/model/ThinEvent.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.stripe.model; - -import com.google.gson.annotations.SerializedName; -import java.time.Instant; -import lombok.Getter; - -/** - * ThinEvent represents the json that's delivered from an Event Destination. It's a basic class with - * no additional methods or properties. Use it to check basic information about a delivered event. - * If you want more details, use `stripeClient.v2().core().events().retrieve(thin_event.id)` to - * fetch the full event object. - */ -@Getter -public class ThinEvent { - /** Unique identifier for the event. */ - @SerializedName("id") - public String id; - - /** The type of the event. */ - @SerializedName("type") - public String type; - - /** Time at which the object was created. */ - @SerializedName("created") - public Instant created; - - /** Livemode indicates if the event is from a production(true) or test(false) account. */ - @SerializedName("livemode") - public Boolean livemode; - - /** [Optional] Authentication context needed to fetch the event or related object. */ - @SerializedName("context") - public String context; - - /** [Optional] Object containing the reference to API resource relevant to the event. */ - @SerializedName("related_object") - public ThinEventRelatedObject relatedObject; - - /** [Optional] Reason for the event. */ - @SerializedName("reason") - public com.stripe.model.v2.Event.Reason reason; -} diff --git a/src/main/java/com/stripe/model/ThinEventRelatedObject.java b/src/main/java/com/stripe/model/ThinEventRelatedObject.java deleted file mode 100644 index 48ea526eca3..00000000000 --- a/src/main/java/com/stripe/model/ThinEventRelatedObject.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.stripe.model; - -import com.google.gson.annotations.SerializedName; -import lombok.Getter; - -@Getter -public class ThinEventRelatedObject { - @SerializedName("id") - public String id; - - @SerializedName("type") - public String type; - - @SerializedName("url") - public String url; -} diff --git a/src/main/java/com/stripe/model/v2/EventNotification.java b/src/main/java/com/stripe/model/v2/EventNotification.java new file mode 100644 index 00000000000..33866cf95e7 --- /dev/null +++ b/src/main/java/com/stripe/model/v2/EventNotification.java @@ -0,0 +1,134 @@ +package com.stripe.model.v2; + +import com.google.gson.JsonObject; +import com.google.gson.annotations.SerializedName; +import com.stripe.StripeClient; +import com.stripe.exception.StripeException; +import com.stripe.model.StripeObject; +import com.stripe.model.v2.Event.RelatedObject; +import com.stripe.net.ApiMode; +import com.stripe.net.ApiResource; +import com.stripe.net.ApiResource.RequestMethod; +import com.stripe.net.RawRequestOptions; +import com.stripe.net.StripeResponse; +import java.time.Instant; +import lombok.AccessLevel; +import lombok.Getter; + +/** + * `EventNotification` represents the common properties for json that's delivered from an Event + * Destination. A concrete child of `EventNotification` will be returned from + * `StripeClient.parseEventNotificaion()`. You will likely want to cast that object to a more + * specific child of `EventNotification`, like `PushedV1BillingMeterErrorReportTriggeredEvent` + */ +@Getter +public abstract class EventNotification { + /** + * For more details about Request, please refer to the API + * Reference. + */ + @Getter + public static class Request { + /** ID of the API request that caused the event. */ + @SerializedName("id") + String id; + + /** The idempotency key transmitted during the request. */ + @SerializedName("idempotency_key") + String idempotencyKey; + } + + @Getter + public static class Reason { + /** Information on the API request that instigated the event. */ + @SerializedName("request") + Request request; + + /** + * Event reason type. + * + *

Equal to {@code request}. + */ + @SerializedName("type") + String type; + } + + /** Unique identifier for the event. */ + @SerializedName("id") + public String id; + + /** The type of the event. */ + @SerializedName("type") + public String type; + + /** Time at which the object was created. */ + @SerializedName("created") + public Instant created; + + /** Livemode indicates if the event is from a production(true) or test(false) account. */ + @SerializedName("livemode") + public Boolean livemode; + + /** [Optional] Authentication context needed to fetch the event or related object. */ + @SerializedName("context") + public String context; + + /** [Optional] Reason for the event. */ + @SerializedName("reason") + public Reason reason; + + @Getter(AccessLevel.NONE) + protected transient StripeClient client; + + /** + * Helper for constructing an Event Notification. Doesn't perform signature validation, so you + * should use {@link com.stripe.StripeClient#parseEventNotification} instead for initial handling. + * This is useful in unit tests and working with EventNotifications that you've already validated + * the authenticity of. + */ + public static EventNotification fromJson(String payload, StripeClient client) { + // don't love the double json parse here, but I don't think we can avoid it? + JsonObject jsonObject = ApiResource.GSON.fromJson(payload, JsonObject.class).getAsJsonObject(); + + Class cls = + EventNotificationClassLookup.eventClassLookup.get(jsonObject.get("type").getAsString()); + if (cls == null) { + cls = UnknownEventNotification.class; + } + + EventNotification e = ApiResource.GSON.fromJson(payload, cls); + e.client = client; + return e; + } + + private RawRequestOptions getRequestOptions() { + if (context == null) { + return null; + } + return new RawRequestOptions.RawRequestOptionsBuilder().setStripeContext(context).build(); + } + + /* retrieves the full payload for an event. Protected because individual push classes use it, but type it correctly */ + protected Event fetchEvent() throws StripeException { + StripeResponse response = + client.rawRequest( + RequestMethod.GET, String.format("/v2/core/events/%s", id), null, getRequestOptions()); + + return (Event) client.deserialize(response.body(), ApiMode.V2); + } + + /** Retrieves the object associated with the event. */ + protected StripeObject fetchRelatedObject(RelatedObject relatedObject) throws StripeException { + if (relatedObject == null) { + // used by UnknownEventNotification, so be a little defensive + return null; + } + + String relativeUrl = relatedObject.getUrl(); + + StripeResponse response = + client.rawRequest(RequestMethod.GET, relativeUrl, null, getRequestOptions()); + + return client.deserialize(response.body(), ApiMode.getMode(relativeUrl)); + } +} diff --git a/src/main/java/com/stripe/model/v2/EventNotificationClassLookup.java b/src/main/java/com/stripe/model/v2/EventNotificationClassLookup.java new file mode 100644 index 00000000000..edd5e489be3 --- /dev/null +++ b/src/main/java/com/stripe/model/v2/EventNotificationClassLookup.java @@ -0,0 +1,28 @@ +// File generated from our OpenAPI spec +package com.stripe.model.v2; + +import java.util.HashMap; +import java.util.Map; + +/** + * Event data class look up used in event deserialization. The key to look up is `object` string of + * the model. + * + *

For internal use by Stripe SDK only. + */ +public final class EventNotificationClassLookup { + public static final Map> eventClassLookup = + new HashMap<>(); + + static { + eventClassLookup.put( + "v1.billing.meter.error_report_triggered", + com.stripe.events.V1BillingMeterErrorReportTriggeredEventNotification.class); + eventClassLookup.put( + "v1.billing.meter.no_meter_found", + com.stripe.events.V1BillingMeterNoMeterFoundEventNotification.class); + eventClassLookup.put( + "v2.core.event_destination.ping", + com.stripe.events.V2CoreEventDestinationPingEventNotification.class); + } +} diff --git a/src/main/java/com/stripe/model/v2/UnknownEventNotification.java b/src/main/java/com/stripe/model/v2/UnknownEventNotification.java new file mode 100644 index 00000000000..f8f2e37559f --- /dev/null +++ b/src/main/java/com/stripe/model/v2/UnknownEventNotification.java @@ -0,0 +1,35 @@ +package com.stripe.model.v2; + +import com.google.gson.annotations.SerializedName; +import com.stripe.exception.StripeException; +import com.stripe.model.StripeObject; +import com.stripe.model.v2.Event.RelatedObject; +import lombok.Getter; + +/** + * Represents a valid EventNotification that the SDK doesn't have a type for. May have a + * `relatedObject` property. + */ +@Getter +public class UnknownEventNotification extends EventNotification { + /** [Optional] Object containing the reference to API resource relevant to the event. */ + @SerializedName("related_object") + public RelatedObject relatedObject; + + /** + * Will make the API call to fetch a related object, if possible. The returned object will have + * the correct type at runtime, but type information about it isn't known at compile time. + */ + public StripeObject fetchRelatedObject() throws StripeException { + return super.fetchRelatedObject(this.relatedObject); + } + + /** + * Will make the API call to fetch a related object, if possible. The returned object will have + * the correct type at runtime, but type information about it isn't known at compile time. + */ + @Override + public Event fetchEvent() throws StripeException { + return super.fetchEvent(); + } +} diff --git a/src/main/java/com/stripe/net/ApiMode.java b/src/main/java/com/stripe/net/ApiMode.java index f9c7e340500..164af7b848a 100644 --- a/src/main/java/com/stripe/net/ApiMode.java +++ b/src/main/java/com/stripe/net/ApiMode.java @@ -2,5 +2,15 @@ public enum ApiMode { V1, - V2 + V2; + + public static ApiMode getMode(String url) { + if (url.startsWith("/v2")) { + return ApiMode.V2; + } + + // some urls, like `/oauth`, don't start with `/v1`. So just assume anything without `/v2` is + // v1. + return ApiMode.V1; + } } diff --git a/src/main/java/com/stripe/net/ApiRequest.java b/src/main/java/com/stripe/net/ApiRequest.java index cc318fb9f46..4125444bb01 100644 --- a/src/main/java/com/stripe/net/ApiRequest.java +++ b/src/main/java/com/stripe/net/ApiRequest.java @@ -19,7 +19,7 @@ private ApiRequest( Map params) { super(baseAddress, method, path, options, usage); this.params = params; - this.apiMode = path.startsWith("/v2") ? ApiMode.V2 : ApiMode.V1; + this.apiMode = ApiMode.getMode(path); } public ApiRequest( diff --git a/src/main/java/com/stripe/net/RawApiRequest.java b/src/main/java/com/stripe/net/RawApiRequest.java index 10ff94aafdd..78a2bc207b8 100644 --- a/src/main/java/com/stripe/net/RawApiRequest.java +++ b/src/main/java/com/stripe/net/RawApiRequest.java @@ -22,7 +22,7 @@ private RawApiRequest( super(baseAddress, method, path, options, usage); this.rawContent = rawContent; this.options = options; - this.apiMode = path.startsWith("/v2") ? ApiMode.V2 : ApiMode.V1; + this.apiMode = ApiMode.getMode(path); } public RawApiRequest( diff --git a/src/test/java/com/stripe/BaseStripeTest.java b/src/test/java/com/stripe/BaseStripeTest.java index 27b243c09f7..8ce1971f866 100644 --- a/src/test/java/com/stripe/BaseStripeTest.java +++ b/src/test/java/com/stripe/BaseStripeTest.java @@ -185,7 +185,7 @@ public static void verifyRequest( * Verifies that a request was made with the provided parameters. * * @param method HTTP method (GET, POST or DELETE) - * @param path request path (e.g. "/v1/charges"). Can also be an abolute URL. + * @param path request path (e.g. "/v1/charges"). Can also be an absolute URL. * @param params map containing the parameters. If null, the parameters are not checked. * @param options request options. If null, the options are not checked. */ @@ -319,7 +319,7 @@ public static void stubRequest( * stripe-mock yet. * * @param method HTTP method (GET, POST or DELETE) - * @param path request path (e.g. "/v1/charges"). Can also be an abolute URL. + * @param path request path (e.g. "/v1/charges"). Can also be an absolute URL. * @param params map containing the parameters. If null, the parameters are not checked. * @param options request options. If null, the options are not checked. * @param typeToken Class of the API resource that will be returned for the stubbed request. @@ -355,7 +355,7 @@ public static void stubRequest( * stripe-mock yet. * * @param method HTTP method (GET, POST or DELETE) - * @param path request path (e.g. "/v1/charges"). Can also be an abolute URL. + * @param path request path (e.g. "/v1/charges"). Can also be an absolute URL. * @param params map containing the parameters. If null, the parameters are not checked. * @param options request options. If null, the options are not checked. * @param typeToken Class of the API resource that will be returned for the stubbed request. diff --git a/src/test/java/com/stripe/StripeClientTest.java b/src/test/java/com/stripe/StripeClientTest.java index a0355e86273..f8c18ce31a7 100644 --- a/src/test/java/com/stripe/StripeClientTest.java +++ b/src/test/java/com/stripe/StripeClientTest.java @@ -2,6 +2,7 @@ import static org.junit.Assert.assertThrows; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -9,20 +10,29 @@ import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import com.stripe.events.V1BillingMeterErrorReportTriggeredEvent; +import com.stripe.events.V1BillingMeterErrorReportTriggeredEventNotification; +import com.stripe.events.V1BillingMeterNoMeterFoundEventNotification; import com.stripe.exception.SignatureVerificationException; import com.stripe.exception.StripeException; -import com.stripe.model.ThinEvent; +import com.stripe.model.billing.Meter; import com.stripe.model.terminal.Reader; +import com.stripe.model.v2.Event; +import com.stripe.model.v2.EventNotification; +import com.stripe.model.v2.UnknownEventNotification; import com.stripe.net.*; +import com.stripe.net.ApiResource.RequestMethod; import java.lang.reflect.Type; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.time.Instant; +import java.util.Collections; import java.util.HashMap; import java.util.Map; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.mockito.stubbing.Answer; @@ -129,7 +139,8 @@ public void checksWebhookSignature() throws InvalidKeyException, NoSuchAlgorithmException, SignatureVerificationException { StripeClient client = new StripeClient("sk_123"); - String payload = "{\n \"id\": \"evt_test_webhook\",\n \"object\": \"event\"\n}"; + String payload = + "{\n \"id\": \"evt_test_webhook\",\n \"type\": \"v1.whatever\",\n \"object\": \"event\"\n}"; String secret = "whsec_test_secret"; Map options = new HashMap<>(); @@ -138,7 +149,7 @@ public void checksWebhookSignature() String signature = WebhookTest.generateSigHeader(options); - ThinEvent e = client.parseThinEvent(payload, signature, secret); + EventNotification e = client.parseEventNotification(payload, signature, secret); assertEquals(e.getId(), "evt_test_webhook"); } @@ -147,44 +158,56 @@ public void failsWebhookVerification() throws InvalidKeyException, NoSuchAlgorithmException, SignatureVerificationException { StripeClient client = new StripeClient("sk_123"); - String payload = "{\n \"id\": \"evt_test_webhook\",\n \"object\": \"event\"\n}"; + String payload = + "{\n \"id\": \"evt_test_webhook\",\n \"type\": \"v1.whatever\",\n \"object\": \"event\"\n}"; String secret = "whsec_test_secret"; String signature = "bad signature"; assertThrows( SignatureVerificationException.class, () -> { - client.parseThinEvent(payload, signature, secret); + client.parseEventNotification(payload, signature, secret); }); } - static final String v2PushEventWithRelatedObject = + static final String v2EventNotificationWithRelatedObject = "{\n" + " \"id\": \"evt_234\",\n" + " \"object\": \"event\",\n" - + " \"type\": \"financial_account.balance.opened\",\n" + + " \"type\": \"v1.billing.meter.error_report_triggered\",\n" + " \"livemode\": false,\n" + + " \"context\": \"org_123\",\n" + " \"created\": \"2022-02-15T00:27:45.330Z\",\n" - + " \"context\": \"context 123\",\n" + " \"related_object\": {\n" - + " \"id\": \"fa_123\",\n" - + " \"type\": \"financial_account\",\n" - + " \"url\": \"/v2/financial_accounts/fa_123\",\n" - + " \"stripe_context\": \"acct_123\"\n" - + " }\n" + + " \"id\": \"meter_123\",\n" + + " \"type\": \"billing.meter\",\n" + + " \"url\": \"/v1/billing/meter/meter_123\"" + + " },\n" + + " \"reason\": { \"type\": \"request\", \"request\": {\n" + + " \"id\": \"abc123\", \"idempotency_key\": \"qwer\"}\n" + + " }" + "}"; - static final String v2PushEventWithoutRelatedObject = + static final String v2EventNotificationWithoutRelatedObject = "{\n" + " \"id\": \"evt_234\",\n" + " \"object\": \"event\",\n" - + " \"type\": \"financial_account.balance.opened\",\n" + + " \"type\": \"v1.billing.meter.no_meter_found\",\n" + + " \"livemode\": false,\n" + + " \"created\": \"2022-02-15T00:27:45.330Z\"\n" + + "}"; + + static final String v2UnknownEventNotification = + "{\n" + + " \"id\": \"evt_234\",\n" + + " \"object\": \"event\",\n" + + " \"type\": \"v1.imaginary_event\",\n" + " \"livemode\": false,\n" + " \"created\": \"2022-02-15T00:27:45.330Z\"\n" + "}"; @Test - public void parsesThinEventWithoutRelatedObject() + public void parsesEventNotificationWithoutRelatedObject() throws InvalidKeyException, NoSuchAlgorithmException, SignatureVerificationException { StripeClient client = new StripeClient("sk_123"); @@ -192,22 +215,22 @@ public void parsesThinEventWithoutRelatedObject() String secret = "whsec_test_secret"; Map options = new HashMap<>(); - options.put("payload", v2PushEventWithoutRelatedObject); + options.put("payload", v2EventNotificationWithoutRelatedObject); options.put("secret", secret); String signature = WebhookTest.generateSigHeader(options); - ThinEvent baseThinEvent = - client.parseThinEvent(v2PushEventWithoutRelatedObject, signature, secret); - assertNotNull(baseThinEvent); - assertEquals("evt_234", baseThinEvent.getId()); - assertEquals("financial_account.balance.opened", baseThinEvent.getType()); - assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), baseThinEvent.created); - assertNull(baseThinEvent.context); - assertNull(baseThinEvent.relatedObject); + EventNotification eventNotification = + client.parseEventNotification(v2EventNotificationWithoutRelatedObject, signature, secret); + assertNotNull(eventNotification); + assertEquals("evt_234", eventNotification.getId()); + assertEquals("v1.billing.meter.no_meter_found", eventNotification.getType()); + assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), eventNotification.created); + assertNull(eventNotification.context); + assertInstanceOf(V1BillingMeterNoMeterFoundEventNotification.class, eventNotification); } @Test - public void parsesThinEventWithRelatedObject() + public void parsesEventNotificationWithRelatedObject() throws InvalidKeyException, NoSuchAlgorithmException, SignatureVerificationException { StripeClient client = new StripeClient("sk_123"); @@ -215,21 +238,69 @@ public void parsesThinEventWithRelatedObject() String secret = "whsec_test_secret"; Map options = new HashMap<>(); - options.put("payload", v2PushEventWithRelatedObject); + options.put("payload", v2EventNotificationWithRelatedObject); options.put("secret", secret); String signature = WebhookTest.generateSigHeader(options); - ThinEvent baseThinEvent = - client.parseThinEvent(v2PushEventWithRelatedObject, signature, secret); - assertNotNull(baseThinEvent); - assertEquals("evt_234", baseThinEvent.getId()); - assertEquals("financial_account.balance.opened", baseThinEvent.getType()); - assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), baseThinEvent.created); - assertEquals("context 123", baseThinEvent.context); - assertNotNull(baseThinEvent.relatedObject); - assertEquals("fa_123", baseThinEvent.relatedObject.id); - assertEquals("financial_account", baseThinEvent.relatedObject.type); - assertEquals("/v2/financial_accounts/fa_123", baseThinEvent.relatedObject.url); + EventNotification eventNotification = + client.parseEventNotification(v2EventNotificationWithRelatedObject, signature, secret); + assertNotNull(eventNotification); + assertEquals("evt_234", eventNotification.getId()); + assertEquals("v1.billing.meter.error_report_triggered", eventNotification.getType()); + assertEquals(Instant.parse("2022-02-15T00:27:45.330Z"), eventNotification.created); + assertEquals("org_123", eventNotification.context); + assertInstanceOf(V1BillingMeterErrorReportTriggeredEventNotification.class, eventNotification); + assertEquals("request", eventNotification.getReason().getType()); + assertEquals("abc123", eventNotification.getReason().getRequest().getId()); + + V1BillingMeterErrorReportTriggeredEventNotification e = + (V1BillingMeterErrorReportTriggeredEventNotification) eventNotification; + + assertNotNull(e.getRelatedObject()); + assertEquals("meter_123", e.getRelatedObject().getId()); + assertEquals("billing.meter", e.getRelatedObject().getType()); + assertEquals("/v1/billing/meter/meter_123", e.getRelatedObject().getUrl()); + } + + @Test + public void parsesUnknownEventNotification() + throws InvalidKeyException, NoSuchAlgorithmException, StripeException { + + StripeClient client = new StripeClient(networkSpy); + + // the existing stubRequest doesn't support rawRequest, so we'll stub manually here + Mockito.doAnswer( + invocation -> { + RawApiRequest request = invocation.getArgument(0); + String path = request.getPath(); + + if (request.getMethod() == RequestMethod.GET + && path.equals("/v2/core/events/evt_234")) { + return new StripeResponse( + 200, + HttpHeaders.of(Collections.emptyMap()), + "{\"id\": \"evt_234\", \"object\": \"v2.core.event\",\"type\": \"v1.imaginary\",\"data\": {}}"); + } + + throw new Exception( + String.format( + "Unexpected rawRequest made: %s %s", request.getMethod(), request.getPath())); + }) + .when(networkSpy) + .rawRequest(Mockito.any()); + + EventNotification eventNotification = + EventNotification.fromJson(v2UnknownEventNotification, client); + + assertNotNull(eventNotification); + assertEquals("evt_234", eventNotification.getId()); + assertEquals("v1.imaginary_event", eventNotification.getType()); + assertInstanceOf(UnknownEventNotification.class, eventNotification); + UnknownEventNotification e = (UnknownEventNotification) eventNotification; + + assertNull(e.getRelatedObject()); + assertNull(e.fetchRelatedObject()); // doesn't work, but doesn't throw + assertInstanceOf(Event.class, e.fetchEvent()); } @Test @@ -277,4 +348,56 @@ public void stripeClientWithStripeContextInV1Api() throws StripeException { assertEquals("ctx", stripeRequest.headers().firstValue("Stripe-Context").get()); }); } + + @Test + public void parseEventNotificationAndPull() + throws StripeException, InvalidKeyException, NoSuchAlgorithmException { + StripeClient client = new StripeClient(networkSpy); + + // the existing stubRequest doesn't support rawRequest, so we'll stub manually here + Mockito.doAnswer( + invocation -> { + RawApiRequest request = invocation.getArgument(0); + String path = request.getPath(); + + if (request.getMethod() == RequestMethod.GET) { + if (path.equals("/v2/core/events/evt_234")) { + return new StripeResponse( + 200, + HttpHeaders.of(Collections.emptyMap()), + "{\"id\": \"evt_234\", \"object\": \"v2.core.event\",\"type\": \"v1.billing.meter.error_report_triggered\",\"data\": {}}"); + } else if (path.equals("/v1/billing/meter/meter_123")) { + return new StripeResponse( + 200, + HttpHeaders.of(Collections.emptyMap()), + "{\"id\": \"meter_123\", \"object\": \"billing.meter\", \"display_name\": \"Test Meter\"}"); + } + } + throw new Exception( + String.format( + "Unexpected rawRequest made: %s %s", request.getMethod(), request.getPath())); + }) + .when(networkSpy) + .rawRequest(Mockito.any()); + + EventNotification eventNotification = + EventNotification.fromJson(v2EventNotificationWithRelatedObject, client); + + assertInstanceOf(V1BillingMeterErrorReportTriggeredEventNotification.class, eventNotification); + + V1BillingMeterErrorReportTriggeredEventNotification en = + (V1BillingMeterErrorReportTriggeredEventNotification) eventNotification; + + assertInstanceOf(V1BillingMeterErrorReportTriggeredEvent.class, en.fetchEvent()); + assertInstanceOf(Meter.class, en.fetchRelatedObject()); + + // we should have made 2 API requests + ArgumentCaptor requestCaptor = ArgumentCaptor.forClass(RawApiRequest.class); + Mockito.verify(networkSpy, Mockito.times(2)).rawRequest(requestCaptor.capture()); + + // and we should have included 'Stripe-Context' in both + for (RawApiRequest v : requestCaptor.getAllValues()) { + assertEquals("org_123", v.getOptions().getStripeContext()); + } + } }