feat: support the reworked bulk contact import (TPL-2105) - #8
Merged
Conversation
Adds the per-contact bulk create shape, the new bulk create response fields, the two bulk topic endpoints, and the duplicate-create 409. All additive: the builder now accepts either `emails` or `contacts` and validates that exactly one is present, and the response's collection getters never return null, so it also reads a pre-TPL-2105 body. ContactAlreadyExistsException extends LettrApiException, so existing catch blocks keep catching duplicates unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Tracks the API changes on
lettr-apivojta/tpl-2105-contact-import-bulk(PR #412). Mirrors what shipped inlettr-php2.5.0,lettr-node(#19),lettr-python(#8) andlettr-go(#7).Everything is additive — code written against 1.4.0 keeps compiling and sends the exact same payloads.
What's in here
Per-contact bulk create.
BulkCreateAudienceContactsOptionstakes a second shape where each contact carries its own properties, lists and topic subscriptions:emailsbecame optional so the builder can host the alternative;build()enforces that exactly one of the two is present. New types:BulkAudienceContactRow,AudienceTopicSubscription(withoptIn/optOut),AudienceTopicSubscriptionState.AudienceTopicSubscriptionStateis deliberately separate fromAudienceTopicDefaultSubscription— one is what a request should do with a topic, the other is how the topic behaves for a contact that says nothing. AnoptOuton an auto-subscribe topic suppresses the auto-subscription in the same request instead of needing a second call.Bulk create reports what happened per row.
BulkCreateAudienceContactsResponsegainsgetUpdated(),getErrorCount(),getErrors(),getContacts(), plushasErrors()/getContactIds()/findIdFor(email). Ids come back in submission order, so chaining intobulkAttachToLists/bulkSubscribeToTopicsneeds no lookup.Two things worth knowing, both covered by tests:
getErrors(); the rest of the batch still commits and the call still returns 201. Nothing throws — callers must checkhasErrors().getAlreadyExisted()andgetUpdated()overlap by design. They answer different questions, so they don't sum to the row count: a contact that already existed and got attached to a list is counted in both.The collection getters never return
null, so the response also reads a pre-TPL-2105 body.Bulk topic subscribe/unsubscribe —
bulkSubscribeToTopics/bulkUnsubscribeFromTopics, mirroring thebulkAttachToLists/bulkDetachFromListspair. Both process everycontactIds×topicIdscombination (up to 1000 × 50). The unsubscribe reuses the existingHttpClient.delete(path, body, type)overload thatbulkDetachFromListsalready relies on, so DELETE-with-a-body needed no plumbing.Duplicate creates are now a 409, not a 500.
create()throwsContactAlreadyExistsException, carrying the collidinggetEmail(). It extendsLettrApiException, so existing catch blocks keep working; a 409 with any other error code stays a plainLettrApiException.send_errorfor this endpoint should be corrected.Notes
BulkAudienceContactError.getErrorCode()stays a rawString(withgetCode()for the typedBulkAudienceContactErrorCode) so a code added server-side doesn't fail to parse — Gson would otherwise deserialize an unknown enum constant tonullsilently.update_existingis a boxedBooleanleft null when false, so a legacy payload serializes byte-identically. Pinned by a test asserting the exact JSON string.[Unreleased]section; perRELEASING.mdthegradle.propertiesversion bump happens at release time, so no version files are touched here.Test plan
./gradlew clean test— 189 pass, including 12 new cases: the per-contact shape's serialization, the legacy payload's exact JSON, the either/or builder guard, deserializing the new response fields, omitted-field tolerance, partial success, an unknown per-row error code, both topic responses, and the exception hierarchy.🤖 Generated with Claude Code