Summary
Calling subscriptions.changePlan shortly after a previous changePlan (before its prorated charge has settled) causes Dodo to return a 409 with this body:
{ "code": "PREVIOUS_PAYMENT_PENDING", "message": "Cannot change plan as previous payment is not successful yet" }
The Java SDK currently surfaces this as a generic DodoPaymentsException, identical in type to a permanently-rejected card. Consumers can't programmatically distinguish "wait
and retry" from "the card was declined for real" without parsing the error message string, which is fragile.
This issue requests two things:
- A typed exception (e.g. PreviousPaymentPendingException or PaymentInFlightException) for this specific code, so callers can implement appropriate retry-with-backoff logic
and surface a meaningful error to end users.
- A short note in the docs for subscriptions.changePlan calling out that change-plan calls cannot overlap with a prior in-flight prorated charge — currently the constraint is
undocumented and only discoverable by hitting the 409 in production.
Reproduction
Sequential calls in test mode, ~1 second apart, against the same subscription:
// First call: succeeds, fires a prorated charge.
sdk.subscriptions().changePlan(SubscriptionChangePlanParams.builder()
.subscriptionId("sub_xxx")
.updateSubscriptionPlanReq(UpdateSubscriptionPlanReq.builder()
.productId("prod_xxx")
.quantity(1)
.prorationBillingMode(ProrationBillingMode.PRORATED_IMMEDIATELY)
.addAddon(AttachAddon.builder().addonId("adn_seats").quantity(3).build())
.build())
.build());
Thread.sleep(1000);
// Second call: throws DodoPaymentsException with the 409 body above.
sdk.subscriptions().changePlan(SubscriptionChangePlanParams.builder()
.subscriptionId("sub_xxx")
// … same shape, quantity 2 …
.build());
Why this matters
In our use case (a SaaS adding/removing per-client seats), it's plausible for an admin to upgrade their seat count and within a few minutes realise they want one fewer. With
the current SDK behaviour we'd have to inspect the exception's response body string (code=PREVIOUS_PAYMENT_PENDING) to give them an accurate message ("your previous change is
still processing — try again in a moment") rather than the misleading default ("card declined"). A typed exception would make this clean.
Environment
- com.dodopayments:dodopayments-java:1.86.3
- Java 17, Spring Boot 3.2.0
Summary
Calling subscriptions.changePlan shortly after a previous changePlan (before its prorated charge has settled) causes Dodo to return a 409 with this body:
{ "code": "PREVIOUS_PAYMENT_PENDING", "message": "Cannot change plan as previous payment is not successful yet" }
The Java SDK currently surfaces this as a generic DodoPaymentsException, identical in type to a permanently-rejected card. Consumers can't programmatically distinguish "wait
and retry" from "the card was declined for real" without parsing the error message string, which is fragile.
This issue requests two things:
and surface a meaningful error to end users.
undocumented and only discoverable by hitting the 409 in production.
Reproduction
Sequential calls in test mode, ~1 second apart, against the same subscription:
// First call: succeeds, fires a prorated charge.
sdk.subscriptions().changePlan(SubscriptionChangePlanParams.builder()
.subscriptionId("sub_xxx")
.updateSubscriptionPlanReq(UpdateSubscriptionPlanReq.builder()
.productId("prod_xxx")
.quantity(1)
.prorationBillingMode(ProrationBillingMode.PRORATED_IMMEDIATELY)
.addAddon(AttachAddon.builder().addonId("adn_seats").quantity(3).build())
.build())
.build());
Thread.sleep(1000);
// Second call: throws DodoPaymentsException with the 409 body above.
sdk.subscriptions().changePlan(SubscriptionChangePlanParams.builder()
.subscriptionId("sub_xxx")
// … same shape, quantity 2 …
.build());
Why this matters
In our use case (a SaaS adding/removing per-client seats), it's plausible for an admin to upgrade their seat count and within a few minutes realise they want one fewer. With
the current SDK behaviour we'd have to inspect the exception's response body string (code=PREVIOUS_PAYMENT_PENDING) to give them an accurate message ("your previous change is
still processing — try again in a moment") rather than the misleading default ("card declined"). A typed exception would make this clean.
Environment