feat: support the reworked bulk contact import (TPL-2105) - #13
Merged
Merged
Conversation
Adds SDK coverage for the additive API changes on lettr-api PR #412, plus the duplicate-create fix. Everything here is backward compatible: code written against 2.4.0 keeps compiling and sends byte-identical payloads. Bulk create now supports a per-contact shape alongside the flat `emails` list. `BulkCreateAudienceContactsData` keeps `(emails, listId, properties)` as its leading positional parameters and appends `contacts`, `listIds`, `topics` and `updateExisting` as optional ones, with `forEmails()` and `forContacts()` named constructors to make the chosen shape explicit. `update_existing` is only emitted when true so legacy payloads are unchanged. `BulkStoreAudienceContactsResult` gains `updated`, `errorCount`, `errors` and `contacts`, all defaulted so the DTO still reads a pre-TPL-2105 response, plus `hasErrors()`, `contactIds()` and `idFor()`. A bulk create can partially succeed and still return 201, so callers must check `hasErrors()` rather than the status. Also adds the bulk topic subscribe/unsubscribe endpoints, and maps the duplicate-email 409 to a dedicated `ContactAlreadyExistsException` (a subclass of `ConflictException`, which is no longer final). To route on it, every `ApiException` now exposes the response `error_code`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI resolves laravel/pint fresh (composer.lock is gitignored) and picked up 1.30.5, whose `fully_qualified_strict_types` also rewrites FQCNs inside docblocks. Reworded the `@see` tags on the two files that referenced AudienceContactService so they no longer pull a service import into a DTO and an exception, and imported Enums\ErrorCode in ApiException. 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.
Adds SDK coverage for the reworked bulk contact import and the duplicate-create fix from lettr-api PR #412. Bumps to 2.5.0 (minor).
Important
Do not tag
v2.5.0until API PR #412 is merged and deployed. The API changes are additive, so this branch is safe to merge and review at any time — but a released 2.5.0 would hand usersbulkSubscribeTopics()/bulkUnsubscribeTopics()against endpoints that 404, and aContactAlreadyExistsExceptionthat never fires.Backward compatibility
Everything is additive. Code written against 2.4.0 keeps compiling and sends byte-identical payloads:
BulkCreateAudienceContactsDatakeeps(emails, listId, properties)as its leading positional parameters; the new fields are appended as optional ones.update_existingis only emitted whentrue, so a legacy call's JSON is unchanged.BulkStoreAudienceContactsResult's new fields all default, so the DTO also parses a pre-TPL-2105 response body.ConflictExceptionlost itsfinal(BC), and the new exception subclasses it — existingcatch (ConflictException)/catch (ApiException)handlers are unaffected.Bulk create — both shapes on one DTO
Two named constructors make the chosen shape explicit:
An empty payload (neither shape filled) now throws
InvalidValueExceptioninstead of being sent to the API.Result reporting
BulkStoreAudienceContactsResultgainsupdated,errorCount,errors[](BulkAudienceContactError) andcontacts[](BulkAudienceContactRef), plushasErrors(),contactIds()andidFor($email)(case-insensitive, since the API normalizes addresses).The two traps from the API notes are documented on the class and in the CHANGELOG:
errors; the rest of the batch commits. Callers must checkhasErrors(), not the status.alreadyExistedandupdatedoverlap by design and do not sum to the row count.New endpoints
bulkSubscribeTopics()/bulkUnsubscribeTopics()on$lettr->audience->contacts(), mirroring the existingbulkAttachLists()/bulkDetachLists()pair. Feed them$result->contactIds()from a bulk create — no id lookup needed. One sharedBulkAudienceContactTopicsDataserves both directions; the lists pair has two identical DTOs, but duplicating that seemed worse than the small inconsistency.409 on duplicate create
ContactAlreadyExistsException extends ConflictException, carrying the colliding->email. A 409 with any other error code stays a plainConflictException.To route on the code,
ApiExceptionnow exposeserrorCode()— threaded through the Client for every mapped status, not just 409, so it is uniformly available rather than a special case on one exception. This is the one piece slightly beyond the strict scope of the API changes.Note for downstream consumers: if a retry policy retries 5xx, duplicate creates are no longer retried (they used to escape as a 500 with the misleading
send_errorcode).Notes for review
AudienceTopicSubscriptionStaterather than reusingAudienceTopicDefaultSubscription. Same two values, but one describes a topic's behavior for new contacts and the other is an instruction in a request — collapsing them would makeopt_outread as "this topic auto-subscribes" at the call site, the opposite of what it does there.BulkAudienceContactError->errorCodeis typedBulkAudienceContactErrorCode|stringso a server-side code addition survives as a raw string instead of throwingValueError(same pattern asCampaignStatus).MockTransportergained a$throwshook for testing error translation.Verification
pint,phpstan(level 8) andpestall pass — 238 tests, 733 assertions, including new coverage for both request shapes, the legacy-response path, the empty-payload guard, both topic-bulk endpoints, and both 409 branches. Also smoke-checked 2.4.0-style positional and named calls to confirm the payloads are unchanged.🤖 Generated with Claude Code