Skip to content

Merge the CustomerService / CustomerThread endpoints into one domain PR - #418

Open
PrestaEdit wants to merge 9 commits into
PrestaShop:devfrom
PrestaEdit:domain/customer-service
Open

PrestaEdit wants to merge 9 commits into
PrestaShop:devfrom
PrestaEdit:domain/customer-service

Conversation

@PrestaEdit

Copy link
Copy Markdown
Contributor
Questions Answers
Branch? dev
Description? Consolidates the five pending CustomerService / CustomerThread PRs into one domain PR, fixes a wrong URI prefix and moves every assertion onto the API
Type? new feature
BC breaks? no
Deprecations? no
Fixed ticket? Related to PrestaShop/PrestaShop#39630
How to test? See below
Sponsor company PrestaEdit

What this PR does

Merges #244, #291, #323, #281 and #381 into one PR, following the mutualisation done on the Product domain in #410.

Endpoints

Method URI CQRS Scope
GET /customer-threads/{customerThreadId}/details GetCustomerThreadForViewing customer_service_read
PUT /customer-threads/{customerThreadId}/set-status UpdateCustomerThreadStatusCommand customer_service_write
PUT /customer-threads/{customerThreadId}/messages ReplyToCustomerThreadCommand customer_service_write
POST /customer-threads/{customerThreadId}/forwards ForwardCustomerThreadCommand customer_service_write
DELETE /customer-threads/{customerThreadId} DeleteCustomerThreadCommand customer_service_write
DELETE /customer-threads/bulk-delete BulkDeleteCustomerThreadCommand customer_service_write
GET /customer-services/signatures/{languageId} GetCustomerServiceSignature customer_service_read

Why #381 is in here

The original grouping put #281 and #381 together because both classes sat in Resources/CustomerService/. That namespace was the problem, not the grouping.

ApiResourceUriTemplateRector derives the first URI segment from the resource namespace. Because CustomerThreadForward was declared under CustomerService, the rule forced /customer-services/{customerThreadId}/forwards — a customer thread id addressed under the customer-services prefix, while the other four thread endpoints used /customer-threads/{customerThreadId}/.... The source branch even carries a commit named "Fix Rector ApiResourceUriTemplateRector (customer-services prefix)", which bent the URI to satisfy the rule instead of moving the class.

Here the class moves to the CustomerThread namespace and the URI becomes /customer-threads/{customerThreadId}/forwards. Same fix for its scope: it declared customer_thread_write, which exists nowhere else — every other resource in the domain uses customer_service_write.

CustomerServiceSignature genuinely belongs to CustomerService (/customer-services/signatures/{languageId}) and stays where it is.

Tests

The three customer-thread test classes become one CustomerThreadEndpointTest.

On fixtures: customer threads are created by the front-office contact form. The CustomerService domain exposes no "add thread" command, so there is no Admin API way to create one — a thread still has to be seeded through the legacy CustomerThread object. What changed is that this now happens in one documented helper instead of three slightly different copies, and every assertion goes back through the API:

Was Now
new \CustomerThread($id)->status after set-status the actions of the details endpoint — an open thread offers closed, a closed one offers open
SELECT COUNT(*) FROM ps_customer_message after reply the messages of the details endpoint, content included
Validate::isLoadedObject() after delete / bulk-delete the details endpoint answering 404

The forward endpoint also gets an actual test — #381 shipped with none, only the getProtectedEndpoints() declaration. Forwarding builds its email from the thread's last message, so the test chains it after the reply, which is only possible now that both endpoints live in the same PR.

⚠️ Core dependency

POST /customer-threads/{customerThreadId}/forwards requires PrestaShop/PrestaShop#42047. ForwardCustomerThreadCommand::__construct() is private on the current cores — the command is only reachable through its toAnotherEmployee() / toSomeoneElse() named constructors, which the serializer cannot use. testForwardCustomerThread stays red until that core PR is merged into the target branches. Every other endpoint here is independent of it.

How to test

GET    /customer-threads/{id}/details      -> 200
PUT    /customer-threads/{id}/set-status   -> 204, then the details actions change
PUT    /customer-threads/{id}/messages     -> 204, then the details messages contain the reply
POST   /customer-threads/{id}/forwards     -> 204, then the details gain a message  (needs core #42047)
DELETE /customer-threads/{id}              -> 204, then the details answer 404
DELETE /customer-threads/bulk-delete       -> 204, then each details answers 404
GET    /customer-services/signatures/{id}  -> 200

Covered by CustomerThreadEndpointTest and CustomerServiceSignatureEndpointTest.

Supersedes

All five will be closed once the CI is green here.

PrestaEdit and others added 9 commits August 20, 2026 10:36
Add the Admin API endpoints to manage customer service threads:

- PUT    /customer-threads/{customerThreadId}/set-status  (UpdateCustomerThreadStatusCommand)
- DELETE /customer-threads/{customerThreadId}             (DeleteCustomerThreadCommand)
- DELETE /customer-threads/bulk-delete                    (BulkDeleteCustomerThreadCommand)

set-status takes a {status} body (open/closed/pending1/pending2) mapped to the command
newCustomerThreadStatus argument; an invalid status returns 422. There is no add command for
threads (they are created from customer messages), so the integration test seeds threads via the
CustomerThread model. The reply/forward actions (which send e-mails) and the rich
GetCustomerThreadForViewing read are intentionally left out of this first resource.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Expose GetCustomerThreadForViewing as GET /customer-threads/{customerThreadId}/details
(scope customer_service_read). Mirrors the CustomerDetails heavy-aggregate read:
the nested CustomerThreadView result (customer information, available actions,
messages and timeline) is mapped to array properties, value objects collapse to
their scalar id. 404 maps from CustomerThreadNotFoundException.

Related to PrestaShop/PrestaShop#39630

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fill the ReplyToCustomerThreadCommand gap: PUT /customer-threads/{customerThreadId}/messages
(scope customer_service_write) to reply to a customer service thread. The test disables
real mail sending (PS_MAIL_METHOD) so the reply email succeeds without an SMTP server.

Related to PrestaShop/PrestaShop#39630

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
GET /customer-services/signatures/{languageId} (GetCustomerServiceSignature) returns the customer
service e-mail signature for a language. The scalar result is mapped to the signature property
(ShowcaseCard pattern). The integration test reads it for the default language.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds POST /customer-threads/{customerThreadId}/forwards using CQRSCreate
with ForwardCustomerThreadCommand. Body: {comment, employeeId? | email?}
— pass one of the two to select the forwarding target.

Safe in CI: test env has PS_MAIL_METHOD = METHOD_DISABLE (3) so
Mail::send returns true without contacting an MTA.

Depends on PrestaShop/PrestaShop#42047 which exposes the Command's
public constructor. Until #42047 lands the resource is registered but
Symfony can't denormalize the request body at runtime.

Related to PrestaShop/PrestaShop#39630

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Consolidates PrestaShop#244, PrestaShop#291, PrestaShop#323, PrestaShop#281 and PrestaShop#381.

PrestaShop#381 had to join the four others rather than stay with PrestaShop#281: it declared
/customer-services/{customerThreadId}/forwards, addressing a customer THREAD under
the customer-services prefix, only because the class sat in the CustomerService
namespace and ApiResourceUriTemplateRector derives the first segment from it. The
class moves to the CustomerThread namespace and the URI becomes
/customer-threads/{customerThreadId}/forwards, which is both correct and Rector
compliant. Its scope also becomes customer_service_write: customer_thread_write
existed nowhere else in the domain.

Tests: the three thread test classes are merged into one. Customer threads are
created by the front-office contact form and the CustomerService domain has no add
command, so a thread still has to be seeded through the legacy object — but that is
now the single place where it happens, and every assertion goes back through the API:

- set-status is asserted through the actions of the details endpoint (an open thread
  offers "close", a closed one offers "re-open") instead of reloading the ObjectModel
- reply is asserted through the messages of the details endpoint instead of a raw
  COUNT on ps_customer_message
- delete and bulk-delete are asserted by the details endpoint answering 404 instead
  of Validate::isLoadedObject()
- the forward endpoint gets an actual test: it had none, only the protected-endpoint
  declaration

The forward endpoint requires PrestaShop/PrestaShop#42047.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-project-automation github-project-automation Bot moved this to Ready for review in PR Dashboard Aug 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Ready for review

Development

Successfully merging this pull request may close these issues.

2 participants