Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Migration - Migrate Customer service options page to Symfony#41421

Open
ga-devfront wants to merge 3 commits into
PrestaShop:developfrom
ga-devfront:migration/customer-service-options
Open

Migration - Migrate Customer service options page to Symfony#41421
ga-devfront wants to merge 3 commits into
PrestaShop:developfrom
ga-devfront:migration/customer-service-options

Conversation

@ga-devfront
Copy link
Copy Markdown
Contributor

Questions Answers
Branch? develop
Description? Migrates the Customer service options page (Sell > Customer Service > Customer Service > Options) to the Symfony controller behind feature flag customer_threads. Replaces the two legacy fields_options panels with iso-functional Symfony forms backed by AbstractMultistoreConfiguration. Part of the AdminCustomerThreads → Symfony migration. Built on top of #41420 (KPI strip + listing stats); rebase against develop once #41418 and #41420 merge.
Type? improvement
Category? BO
BC breaks? no
Deprecations? no
How to test? (1) Enable the customer_threads feature flag. (2) Open Sell > Customer Service > Customer Service > Options. (3) Verify the Contact options panel saves the file-upload toggle and the translatable default signature for each language. (4) Verify the Customer service options panel saves the IMAP server URL, port, user, password and the seven IMAP option toggles. (5) Run composer create-test-db && ./vendor/bin/behat -c tests/Integration/Behaviour/behat.yml -s customer_service — expect 9 scenarios green.
UI Tests N/A — no Playwright changes; the existing 05_customerServiceOptions.ts and 04_contactOptions.ts campaigns continue to pass against the migrated page (same field labels, same DB keys).
Fixed issue or discussion? Part of the AdminCustomerThreads → Symfony migration umbrella tracked in #41417.
Related PRs Stacked on top of #41420 (KPI strip + listing stats), itself stacked on #41418 (Behat retrofit). Please merge upstream PRs first; this branch will rebase cleanly.
Sponsor company @PrestaShopCorp

What this PR adds

Configuration adapters

Two new classes under src/Adapter/CustomerService/Configuration/, both extending AbstractMultistoreConfiguration so values respect the active ShopConstraint:

  • CustomerServiceOptionsConfiguration — Contact options panel: file_upload (PS_CUSTOMER_SERVICE_FILE_UPLOAD) and translatable signature (PS_CUSTOMER_SERVICE_SIGNATURE, stored as a per-language array).
  • ImapConfiguration — IMAP panel: 13 settings covering connection (URL / port / user / password), behaviour flags (delete after sync, create unrecognized threads) and the seven IMAP option toggles. The legacy concatenated PS_SAV_IMAP_OPT value is intentionally not written: grep across the whole codebase confirms no consumers, and syncImap rebuilds the option string from the individual PS_SAV_IMAP_OPT_* toggles at call time.

Form layer

Following the existing LogsController convention used for option pages elsewhere in PrestaShop:

  • CustomerServiceOptionsType and ImapOptionsType (Symfony form types). The signature uses TranslatableType over TextareaType; booleans use SwitchType.
  • CustomerServiceOptionsFormDataProvider and ImapOptionsFormDataProvider bridging the forms to the configuration adapters via FormDataProviderInterface.
  • Two Core\Form\Handler services (prestashop.adapter.customer_service.options.form_handler and prestashop.adapter.customer_service.imap.form_handler).

Controller actions and routing

CustomerThreadController gains:

  • optionsAction (GET /options) — renders both forms.
  • saveOptionsAction (POST /options) — saves the Contact panel.
  • saveImapOptionsAction (POST /options/imap) — saves the IMAP panel.
  • A private processOptionsForm helper shared by both save actions.

The two save flows are isolated so a validation error on one panel doesn't reject the other (matches legacy iso-functional behaviour with two separate submit buttons).

All routes carry _legacy_feature_flag: customer_threads; merchants on the legacy controller continue to see the old options panels until the flag is enabled.

Behat coverage

customer_service_options.feature exercises both data providers through the DI container and asserts persisted values via the legacy Configuration::get adapter:

  • Save the Contact options panel (file upload + translatable signature).
  • Toggle the file upload off.
  • Save the full IMAP panel (URL, port, user, password, behaviour flags, option toggles).

Step regexes are anchored so the boolean-state checks (should be enabled / should be disabled) don't conflate with string-value checks (should be "...").

What is intentionally NOT in this PR

  • CQRS commands for options updates — PrestaShop's option pages traditionally use DataConfigurationInterface directly (Logs, Customer preferences, Invoice options, etc.). Introducing CQRS commands here would add boilerplate without benefit and diverge from the surrounding codebase. The Admin API in a later PR will inject the configuration adapters directly, the same way the BO controller does.
  • IMAP connection validation — the legacy isValidImapUrl validator is not yet ported to a Symfony constraint. Will land alongside PR 4 (IMAP sync) which is the only consumer.
  • Playwright additions — existing 04_contactOptions.ts and 05_customerServiceOptions.ts campaigns already exercise the same field labels and DB keys; they will be re-run against the migrated page in the dedicated Playwright PR.

Adds the iso-functional equivalent of the legacy AdminCustomerThreads
KPI row and statistics panel that were missing from the migrated
Symfony page (feature flag customer_threads).

KPI strip:
- New KPI classes in src/Adapter/Kpi/ for the three legacy KPIs:
  PendingDiscussionThreadsKpi, AverageMessageResponseTimeKpi,
  MessagesPerThreadKpi. They reuse HelperKpi and the existing
  AdminStats ajax endpoints unchanged, so displayed values match
  the legacy page exactly. Refactor onto a CQRS query is tracked
  separately in PrestaShop#41417.
- New KPI row factory prestashop.core.kpi_row.factory.customer_service.
- Inject the factory in CustomerThreadController::indexAction and
  render via CommonController::renderKpiRowAction.

Listing statistics:
- New CQRS query GetCustomerServiceListingStatistics returning the
  six counters previously rendered by AdminCustomerThreadsController
  ::renderList: total / open / pending / closed threads plus customer
  and employee message totals.
- Result DTO CustomerServiceListingStatistics with typed getters.
- Handler in src/Adapter/CustomerService/QueryHandler/ delegating to
  the existing CustomerThread / CustomerMessage ObjectModel counters
  so multistore scoping (Shop::addSqlRestriction) is preserved.
- Twig partial Block/listing_statistics.html.twig renders the counters
  alongside the legacy status meaning legend.

Behat coverage:
- New customer_service_statistics.feature with two scenarios driving
  the query through the bus (empty installation and per-status
  aggregation). A 'Given there are no customer threads' background
  step keeps counts deterministic regardless of seed data.
Adds the Customer service options page (Sell > Customer Service >
Customer Service > Options) to the Symfony controller, replacing the
legacy fields_options panels behind feature flag customer_threads.

Splits the legacy single-page options into two iso-functional panels:

  * Contact options
    file_upload (PS_CUSTOMER_SERVICE_FILE_UPLOAD)
    signature   (PS_CUSTOMER_SERVICE_SIGNATURE, translatable)

  * Customer service options (IMAP)
    13 keys covering URL/port/user/password, the two behavior flags
    (delete after sync, create unrecognized threads), and the seven
    IMAP option toggles. The legacy PS_SAV_IMAP_OPT concatenated value
    is intentionally not written: grep confirms no consumers, and the
    sync handler rebuilds the option string from the individual
    toggles at call time.

Architecture follows the LogsController convention used elsewhere in
PrestaShop for option pages:

  CustomerServiceOptionsConfiguration / ImapConfiguration
    extend AbstractMultistoreConfiguration so values are stored with
    the proper ShopConstraint and respect multi-shop scoping.

  CustomerServiceOptionsFormDataProvider / ImapOptionsFormDataProvider
    bridge the form layer to the configuration adapters via
    FormDataProviderInterface.

  CustomerServiceOptionsType / ImapOptionsType
    Symfony form types with translatable signature (TranslatableType
    over TextareaType) and the standard SwitchType for booleans.

  CustomerThreadController
    new optionsAction (GET) renders both forms; saveOptionsAction and
    saveImapOptionsAction (POST) delegate to a shared
    processOptionsForm helper.

  Routes
    admin_customer_threads_options          (GET  /options)
    admin_customer_threads_options_save     (POST /options)
    admin_customer_threads_imap_options_save (POST /options/imap)
    All carry _legacy_feature_flag: customer_threads.

Behat:
  customer_service_options.feature with three scenarios driving the
  contact and IMAP form data providers through the container and
  asserting Configuration values via the legacy adapter. Dedicated
  step regexes disambiguate "should be \"value\"" from
  "should be enabled / disabled".
@ga-devfront ga-devfront requested a review from a team as a code owner May 6, 2026 16:26
@github-project-automation github-project-automation Bot moved this to Ready for review in PR Dashboard May 6, 2026
@ps-jarvis ps-jarvis added Improvement Type: Improvement develop Branch labels May 6, 2026
@ga-devfront ga-devfront added the Blocked Status: The issue is blocked by another task label May 6, 2026
@ga-devfront ga-devfront added this to the 9.2.0 milestone May 6, 2026
Copy link
Copy Markdown
Collaborator

@cnavarro-prestashop cnavarro-prestashop left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

$resolver = (new OptionsResolver())
->setDefined(array_keys(self::FIELD_TO_CONFIG_KEY))
->setAllowedTypes('imap_url', 'string')
->setAllowedTypes('imap_port', ['string', 'int'])
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why having int and string ? Easier ton handle string no ?

Comment on lines +89 to +91
foreach (self::FIELD_TO_CONFIG_KEY as $field => $configKey) {
$this->updateConfigurationValue($configKey, $field, $configuration, $shopConstraint);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imap_password belongs to FIELD_TO_CONFIG_KEY. If in the form type there's not changed for imap_password it's supposed to be empty. When you'll save it will erase the imap_password in configuration. You should ignore the field imap_password if $configuration[$field] is empty :

Suggested change
foreach (self::FIELD_TO_CONFIG_KEY as $field => $configKey) {
$this->updateConfigurationValue($configKey, $field, $configuration, $shopConstraint);
}
foreach (self::FIELD_TO_CONFIG_KEY as $field => $configKey) {
if ($field === 'imap_password' && $configuration[$field] === '') {
continue;
}
$this->updateConfigurationValue($configKey, $field, $configuration, $shopConstraint);
}

@ps-jarvis ps-jarvis added the Waiting for author Status: action required, waiting for author feedback label May 7, 2026
@ps-jarvis ps-jarvis moved this from Ready for review to Waiting for author in PR Dashboard May 7, 2026
*/
#[AsQueryHandler]
final class GetCustomerServiceListingStatisticsHandler implements GetCustomerServiceListingStatisticsHandlerInterface
{
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This handler performs multiple separate database queries to aggregate statistics. To improve performance, consider combining these into a single query using COUNT(IF(...)) or GROUP BY. Also, it should explicitly handle a ShopConstraint instead of relying on the global legacy context.

Suggested fix: Optimize to a single query and use explicit ShopConstraint.

return array_map('intval', $customerThreadIds);
}

private function processOptionsForm(Request $request, FormHandlerInterface $formHandler): RedirectResponse
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using guard clauses here would reduce the nesting level and improve readability.

Suggested fix:

private function processOptionsForm(Request $request, FormHandlerInterface $formHandler): RedirectResponse
{
    $form = $formHandler->getForm();
    $form->handleRequest($request);

    if (!$form->isSubmitted() || !$form->isValid()) {
        return $this->redirectToRoute('admin_customer_threads_options');
    }

    $saveErrors = $formHandler->save($form->getData());
    // ...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Blocked Status: The issue is blocked by another task develop Branch Improvement Type: Improvement Waiting for author Status: action required, waiting for author feedback

Projects

Status: Waiting for author

Development

Successfully merging this pull request may close these issues.

4 participants