Migration - Migrate Customer service options page to Symfony#41421
Migration - Migrate Customer service options page to Symfony#41421ga-devfront wants to merge 3 commits into
Conversation
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".
cnavarro-prestashop
left a comment
There was a problem hiding this comment.
- comments in #41420
| $resolver = (new OptionsResolver()) | ||
| ->setDefined(array_keys(self::FIELD_TO_CONFIG_KEY)) | ||
| ->setAllowedTypes('imap_url', 'string') | ||
| ->setAllowedTypes('imap_port', ['string', 'int']) |
There was a problem hiding this comment.
Why having int and string ? Easier ton handle string no ?
| foreach (self::FIELD_TO_CONFIG_KEY as $field => $configKey) { | ||
| $this->updateConfigurationValue($configKey, $field, $configuration, $shopConstraint); | ||
| } |
There was a problem hiding this comment.
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 :
| 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); | |
| } |
| */ | ||
| #[AsQueryHandler] | ||
| final class GetCustomerServiceListingStatisticsHandler implements GetCustomerServiceListingStatisticsHandlerInterface | ||
| { |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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());
// ...
}
Sell > Customer Service > Customer Service > Options) to the Symfony controller behind feature flagcustomer_threads. Replaces the two legacyfields_optionspanels with iso-functional Symfony forms backed byAbstractMultistoreConfiguration. Part of the AdminCustomerThreads → Symfony migration. Built on top of #41420 (KPI strip + listing stats); rebase againstdeveloponce #41418 and #41420 merge.customer_threadsfeature flag. (2) OpenSell > 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) Runcomposer create-test-db && ./vendor/bin/behat -c tests/Integration/Behaviour/behat.yml -s customer_service— expect 9 scenarios green.05_customerServiceOptions.tsand04_contactOptions.tscampaigns continue to pass against the migrated page (same field labels, same DB keys).What this PR adds
Configuration adapters
Two new classes under
src/Adapter/CustomerService/Configuration/, both extendingAbstractMultistoreConfigurationso values respect the activeShopConstraint:CustomerServiceOptionsConfiguration— Contact options panel:file_upload(PS_CUSTOMER_SERVICE_FILE_UPLOAD) and translatablesignature(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 concatenatedPS_SAV_IMAP_OPTvalue is intentionally not written: grep across the whole codebase confirms no consumers, andsyncImaprebuilds the option string from the individualPS_SAV_IMAP_OPT_*toggles at call time.Form layer
Following the existing
LogsControllerconvention used for option pages elsewhere in PrestaShop:CustomerServiceOptionsTypeandImapOptionsType(Symfony form types). The signature usesTranslatableTypeoverTextareaType; booleans useSwitchType.CustomerServiceOptionsFormDataProviderandImapOptionsFormDataProviderbridging the forms to the configuration adapters viaFormDataProviderInterface.Core\Form\Handlerservices (prestashop.adapter.customer_service.options.form_handlerandprestashop.adapter.customer_service.imap.form_handler).Controller actions and routing
CustomerThreadControllergains:optionsAction(GET /options) — renders both forms.saveOptionsAction(POST /options) — saves the Contact panel.saveImapOptionsAction(POST /options/imap) — saves the IMAP panel.processOptionsFormhelper 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
submitbuttons).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.featureexercises both data providers through the DI container and asserts persisted values via the legacyConfiguration::getadapter: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
DataConfigurationInterfacedirectly (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.isValidImapUrlvalidator is not yet ported to a Symfony constraint. Will land alongside PR 4 (IMAP sync) which is the only consumer.04_contactOptions.tsand05_customerServiceOptions.tscampaigns already exercise the same field labels and DB keys; they will be re-run against the migrated page in the dedicated Playwright PR.