diff --git a/CHANGELOG.md b/CHANGELOG.md index bbaf66e6..3912803b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,13 @@ before starting to add changes. Use example [placed in the end of the page](#exa ## [Unreleased] +- [PR-189](https://github.com/OS2Forms/os2forms/pull/189) + - Added support for MeMo 1.2 and added additional validation of MeMo actions. +- [PR-202](https://github.com/OS2Forms/os2forms/pull/202) + - Removed non-digits from recipient id in Maestro digital post notifications. +- [PR-191](https://github.com/OS2Forms/os2forms/pull/191) + - Re-throws exception to ensure failed status during Maestro notification job. + ## [4.1.0] 2025-06-03 - [PR-176](https://github.com/OS2Forms/os2forms/pull/176) diff --git a/composer.json b/composer.json index 5f20db3d..8a950713 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "fig/http-message-util": "^1.1", "http-interop/http-factory-guzzle": "^1.0.0", "itk-dev/beskedfordeler-drupal": "^1.0", - "itk-dev/serviceplatformen": "^1.5", + "itk-dev/serviceplatformen": "^1.7.1", "mglaman/composer-drupal-lenient": "^1.0", "os2web/os2web_audit": "^1.0", "os2web/os2web_datalookup": "^2.0", diff --git a/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example.yml b/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example.yml index 6a18f4dd..a942a2d7 100644 --- a/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example.yml +++ b/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example.yml @@ -240,14 +240,10 @@ handlers: actions: - action: INFORMATION - url: 'http://dr.dk' + url: '[site:url]' label: 'Se her!' - action: SELVBETJENING - url: 'https://selvbetjening.aarhuskommune.dk/da/content/book-aarhus' - label: 'Book ressource' - - - action: FORBEREDELSE - url: 'http://tv2.dk' - label: 'Forbered dig med' + url: 'https://eksempel.dk' + label: 'Eksempel' variants: { } diff --git a/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example_2.yml b/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example_2.yml new file mode 100644 index 00000000..3994ece5 --- /dev/null +++ b/modules/os2forms_digital_post/modules/os2forms_digital_post_examples/config/install/webform.webform.os2forms_digital_post_example_2.yml @@ -0,0 +1,245 @@ +langcode: da +status: open +dependencies: + enforced: + module: + - os2forms_digital_post_examples + module: + - os2forms_digital_post_examples +third_party_settings: + webform_revisions: + contentEntity_id: null +weight: 0 +open: null +close: null +uid: 1 +template: false +archive: false +id: os2forms_digital_post_example_2 +title: 'OS2Forms Digital post example with invalid handler URL' +description: 'Simple example form with a digital post handler with invalid handler URL' +category: Example +elements: |- + message: + '#type': textarea + '#title': Message + '#required': true + '#default_value': |- + [current-date:long] + + [random:hash:sha512] + recipient_cpr: + '#type': textfield + '#title': 'Recipient cpr' + '#required': true + '#default_value': '1705880000' + digital_post_content_pdf: + '#type': 'webform_entity_print_attachment:pdf' + '#title': 'Digital post (PDF)' + '#display_on': view + '#filename': hat-og-briller.pdf +css: '' +javascript: '' +settings: + ajax: false + ajax_scroll_top: form + ajax_progress_type: '' + ajax_effect: '' + ajax_speed: null + page: true + page_submit_path: '' + page_confirm_path: '' + page_theme_name: '' + form_title: both + form_submit_once: false + form_open_message: '' + form_close_message: '' + form_exception_message: '' + form_previous_submissions: true + form_confidential: false + form_confidential_message: '' + form_disable_remote_addr: false + form_convert_anonymous: false + form_prepopulate: false + form_prepopulate_source_entity: false + form_prepopulate_source_entity_required: false + form_prepopulate_source_entity_type: '' + form_unsaved: false + form_disable_back: false + form_submit_back: false + form_disable_autocomplete: false + form_novalidate: false + form_disable_inline_errors: false + form_required: false + form_autofocus: false + form_details_toggle: false + form_reset: false + form_access_denied: default + form_access_denied_title: '' + form_access_denied_message: '' + form_access_denied_attributes: { } + form_file_limit: '' + form_attributes: { } + form_method: '' + form_action: '' + share: false + share_node: false + share_theme_name: '' + share_title: true + share_page_body_attributes: { } + submission_label: '' + submission_exception_message: '' + submission_locked_message: '' + submission_log: false + submission_excluded_elements: { } + submission_exclude_empty: false + submission_exclude_empty_checkbox: false + submission_views: { } + submission_views_replace: { } + submission_user_columns: { } + submission_user_duplicate: false + submission_access_denied: default + submission_access_denied_title: '' + submission_access_denied_message: '' + submission_access_denied_attributes: { } + previous_submission_message: '' + previous_submissions_message: '' + autofill: false + autofill_message: '' + autofill_excluded_elements: { } + wizard_progress_bar: true + wizard_progress_pages: false + wizard_progress_percentage: false + wizard_progress_link: false + wizard_progress_states: false + wizard_start_label: '' + wizard_preview_link: false + wizard_confirmation: true + wizard_confirmation_label: '' + wizard_auto_forward: true + wizard_auto_forward_hide_next_button: false + wizard_keyboard: true + wizard_track: '' + wizard_prev_button_label: '' + wizard_next_button_label: '' + wizard_toggle: false + wizard_toggle_show_label: '' + wizard_toggle_hide_label: '' + preview: 0 + preview_label: '' + preview_title: '' + preview_message: '' + preview_attributes: { } + preview_excluded_elements: { } + preview_exclude_empty: true + preview_exclude_empty_checkbox: false + draft: none + draft_multiple: false + draft_auto_save: false + draft_saved_message: '' + draft_loaded_message: '' + draft_pending_single_message: '' + draft_pending_multiple_message: '' + confirmation_type: message + confirmation_url: '' + confirmation_title: '' + confirmation_message: '' + confirmation_attributes: { } + confirmation_back: true + confirmation_back_label: '' + confirmation_back_attributes: { } + confirmation_exclude_query: false + confirmation_exclude_token: false + confirmation_update: false + limit_total: null + limit_total_interval: null + limit_total_message: '' + limit_total_unique: false + limit_user: null + limit_user_interval: null + limit_user_message: '' + limit_user_unique: false + entity_limit_total: null + entity_limit_total_interval: null + entity_limit_user: null + entity_limit_user_interval: null + purge: all + purge_days: 30 + results_disabled: false + results_disabled_ignore: false + results_customize: false + token_view: false + token_update: false + token_delete: false + serial_disabled: false +access: + create: + roles: + - anonymous + - authenticated + users: { } + permissions: { } + view_any: + roles: { } + users: { } + permissions: { } + update_any: + roles: { } + users: { } + permissions: { } + delete_any: + roles: { } + users: { } + permissions: { } + purge_any: + roles: { } + users: { } + permissions: { } + view_own: + roles: { } + users: { } + permissions: { } + update_own: + roles: { } + users: { } + permissions: { } + delete_own: + roles: { } + users: { } + permissions: { } + administer: + roles: { } + users: { } + permissions: { } + test: + roles: { } + users: { } + permissions: { } + configuration: + roles: { } + users: { } + permissions: { } +handlers: + digital_post_sf1601: + id: digital_post_sf1601 + handler_id: digital_post_sf1601 + label: 'Digital post (sf1601)' + notes: '' + status: true + conditions: { } + weight: 0 + settings: + debug: false + memo_message: + type: 'Automatisk Valg' + recipient_element: recipient_cpr + attachment_element: digital_post_content_pdf + sender_label: 'Hilsen fra [site:url-brief]' + message_header_label: SF1601 + memo_actions: + actions: + - + action: INFORMATION + url: 'http://eksempel.dk' + label: 'Se her!' +variants: { } diff --git a/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php b/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php index 201581c5..b0efb2b3 100644 --- a/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php +++ b/modules/os2forms_digital_post/src/Drush/Commands/DigitalPostTestCommands.php @@ -2,6 +2,9 @@ namespace Drupal\os2forms_digital_post\Drush\Commands; +use DigitalPost\MeMo\Action; +use DigitalPost\MeMo\EntryPoint; +use DigitalPost\MeMo\Reservation; use Drupal\Component\Serialization\Yaml; use Drupal\Core\DependencyInjection\AutowireTrait; use Drupal\Core\Utility\Token; @@ -10,10 +13,16 @@ use Drupal\os2forms_digital_post\Helper\Settings; use Drupal\os2forms_digital_post\Model\Document; use Drush\Commands\DrushCommands; +use ItkDev\Serviceplatformen\Service\SF1601\Serializer; use ItkDev\Serviceplatformen\Service\SF1601\SF1601; use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Exception\InvalidOptionException; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\OptionsResolver\Exception\ExceptionInterface; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Options; +use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; /** @@ -44,14 +53,20 @@ public function __construct( * @param array $options * The options. * - * @option string subject - * The subject. Can contain HTML. - * @option string message - * The message to send. Can contain HTML. - * @option string digital-post-type - * The digital post type to use. - * @option bool dump-digital-post-settings - * Dump digital post settings. + * @option subject + * The subject. Can contain HTML. + * @option message + * The message to send. Can contain HTML. + * @option digital-post-type + * The digital post type to use. + * @option dump-digital-post-settings + * Dump digital post settings. + * @option memo-version + * MeMo version (1.1 or 1.2). If not set, a proper default will be used. + * @option action + * MeMo actions, e.g. 'action=INFORMATION&label=Vigtig%20information&entrypoint=https://example.com' + * @option filename + * The main document filename (used to test invalid filenames (cf. https://digitaliser.dk/digital-post/nyhedsarkiv/2024/nov/oeget-validering-i-digital-post)) * * @phpstan-param array $recipients * @phpstan-param array $options @@ -66,6 +81,9 @@ public function send( 'message' => 'This is a test message from os2forms_digital_post sent on [current-date:html_datetime].', 'digital-post-type' => SF1601::TYPE_AUTOMATISK_VALG, 'dump-digital-post-settings' => FALSE, + 'memo-version' => NULL, + 'action' => [], + 'filename' => 'os2forms_digital_post', ], ): void { $io = new SymfonyStyle($this->input(), $this->output()); @@ -89,7 +107,7 @@ public function send( $document = new Document( $content, Document::MIME_TYPE_PDF, - 'os2forms_digital_post.pdf' + $options['filename'] . '.pdf', ); $type = $options['digital-post-type']; @@ -98,21 +116,42 @@ public function send( throw new InvalidArgumentException(sprintf('Invalid type: %s. Must be one of %s.', $quote($type), implode(', ', array_map($quote, SF1601::TYPES)))); } + $meMoVersion = $options['memo-version']; + if ($meMoVersion) { + $meMoVersion = (float) $meMoVersion; + $allowedValues = [SF1601::MEMO_1_1, SF1601::MEMO_1_2]; + if (!in_array($meMoVersion, $allowedValues, TRUE)) { + $quote = static fn($value) => var_export($value, TRUE); + throw new InvalidArgumentException(sprintf( + 'Invalid MeMo version: %s. Must be one of %s.', + $quote($meMoVersion), + implode(', ', array_map($quote, $allowedValues)) + )); + } + } + $io->section('Digital post'); $io->definitionList( ['Type' => $type], ['Subject' => $subject], - ['Message' => $message] + ['Message' => $message], + ['Document' => sprintf('%s (%s)', $document->filename, $document->mimeType)], + ['MeMo version' => $meMoVersion ?? '–'], ); + $actions = array_map($this->buildAction(...), $options['action']); + foreach ($recipients as $recipient) { try { - $io->writeln(sprintf('Recipient: %s', $recipient)); $recipientLookupResult = $this->digitalPostHelper->lookupRecipient($recipient); - $actions = []; $meMoMessage = $this->digitalPostHelper->getMeMoHelper()->buildMessage($recipientLookupResult, $senderLabel, $messageLabel, $document, $actions); + // If a valid memo-version option has been provided, set that version on + // the message. + if ($meMoVersion) { + $meMoMessage->setMemoVersion($meMoVersion); + } $forsendelse = $this->digitalPostHelper->getForsendelseHelper()->buildForsendelse($recipientLookupResult, $messageLabel, $document); @@ -122,7 +161,13 @@ public function send( $forsendelse ); - $io->success(sprintf('Digital post sent to %s', $recipient)); + $io->definitionList( + ['Recipient' => $recipient], + ['Document' => sprintf('%s (%s)', $document->filename, $document->mimeType)], + ['MeMo version' => $meMoMessage->getMemoVersion()], + ); + + $io->success(sprintf('Digital post sent to %s (MeMo %s)', $recipient, $meMoMessage->getMemoVersion())); } catch (\Throwable $throwable) { $io->error(sprintf('Error sending digital post to %s:', $recipient)); @@ -158,4 +203,91 @@ private function dumpDigitalPostSettings(SymfonyStyle $io): void { ]); } + /** + * Build MeMo action. + * + * Lifted from KombiPostAfsendCommand::buildAction(). + * + * @see KombiPostAfsendCommand::buildAction() + */ + private function buildAction(string $spec): Action { + parse_str($spec, $options); + $resolver = $this->getActionOptionsResolver(); + try { + $options = $resolver->resolve($options); + } + catch (ExceptionInterface $exception) { + throw new InvalidOptionException(sprintf( + 'Invalid action %s: %s', + json_encode($spec), + $exception->getMessage() + )); + } + + $action = (new Action()) + ->setActionCode($options['action']) + ->setLabel($options['label']); + if (SF1601::ACTION_AFTALE === $options['action']) { + $reservation = (new Reservation()) + ->setStartDateTime(new \DateTime('+2 days')) + ->setEndDateTime(new \DateTime('+2 days 1 hour')) + ->setLocation('Meeting room 1') + ->setAbstract('Abstract') + ->setDescription('Description') + ->setOrganizerName('Organizer') + ->setOrganizerMail('organizer@example.com') + ->setReservationUUID(Serializer::createUuid()); + $action->setReservation($reservation); + } + elseif ($options['entrypoint']) { + $action->setEntryPoint( + (new EntryPoint()) + ->setUrl($options['entrypoint']) + ); + } + + if ($options['endDateTime']) { + $action->setEndDateTime(new \DateTime($options['endDateTime'])); + } + + return $action; + } + + /** + * Get actions options resolver. + * + * @see KombiPostAfsendCommand::getActionOptionsResolver() + */ + private function getActionOptionsResolver(): OptionsResolver { + $resolver = new OptionsResolver(); + $resolver + ->setRequired([ + 'action', + 'label', + ]) + ->setDefaults([ + 'endDateTime' => NULL, + 'entrypoint' => NULL, + ]) + ->setInfo('action', sprintf('The action name (one of %s)', implode(', ', SF1601::ACTIONS))) + ->setInfo('label', 'The action label') + ->setInfo('endDateTime', 'The end time e.g. "2022-12-02" or "14 days"') + ->setInfo('entrypoint', 'The entry point (an URL)') + ->setAllowedValues('action', static function ($value) { + return in_array($value, SF1601::ACTIONS, TRUE); + }) + ->setNormalizer('entrypoint', static function (Options $options, $value) { + if (NULL === $value && SF1601::ACTION_AFTALE !== $options['action']) { + throw new InvalidOptionsException(sprintf( + 'Action entrypoint is required for all actions but %s', + SF1601::ACTION_AFTALE + )); + } + + return $value; + }); + + return $resolver; + } + } diff --git a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php index 3ec8b7ed..f32c567d 100644 --- a/modules/os2forms_digital_post/src/Helper/MeMoHelper.php +++ b/modules/os2forms_digital_post/src/Helper/MeMoHelper.php @@ -14,6 +14,8 @@ use DigitalPost\MeMo\MessageHeader; use DigitalPost\MeMo\Recipient; use DigitalPost\MeMo\Sender; +use Drupal\Component\Utility\UrlHelper; +use Drupal\Core\StringTranslation\TranslatableMarkup; use Drupal\os2forms_digital_post\Model\Document; use Drupal\os2forms_digital_post\Plugin\WebformHandler\WebformHandlerSF1601; use Drupal\os2web_datalookup\LookupResult\CompanyLookupResult; @@ -175,6 +177,9 @@ private function buildAction(array $options, WebformSubmissionInterface $submiss } elseif ($options['url']) { $url = $this->replaceTokens($options['url'], $submission); + if ($message = self::validateActionUrl($url, $options)) { + throw new \RuntimeException((string) $message); + } $action->setEntryPoint( (new EntryPoint()) ->setUrl($url) @@ -184,4 +189,74 @@ private function buildAction(array $options, WebformSubmissionInterface $submiss return $action; } + /** + * Validate an action URL. + * + * @param string $url + * The URL. + * @param array $options + * The options. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup|null + * A message if the URL is not valid for an action. + * + * @phpstan-param array $options + */ + public static function validateActionUrl(string $url, array $options): ?TranslatableMarkup { + // URL must be absolute and use https (cf. https://digitaliser.dk/digital-post/nyhedsarkiv/2024/nov/oeget-validering-i-digital-post) + if (!UrlHelper::isValid($url, absolute: TRUE)) { + return new TranslatableMarkup('URL @url for action %action must be absolute, i.e. start with https://.', [ + '@url' => $url, + '%action' => self::getTranslatedActionName($options['action']), + ]); + } + elseif ('https' !== parse_url($url, PHP_URL_SCHEME)) { + return new TranslatableMarkup('URL @url for action %action must use the https scheme, i.e. start with https://.', [ + '@url' => $url, + '%action' => self::getTranslatedActionName($options['action']), + ]); + } + + return NULL; + } + + /** + * Translated action names. + * + * @var array|null + * + * @phpstan-var array + */ + private static ?array $translatedActionNames = NULL; + + /** + * Get translated action names. + * + * @return array + * The translated action names. + */ + public static function getTranslatedActionNames(): array { + if (NULL === self::$translatedActionNames) { + self::$translatedActionNames = [ + SF1601::ACTION_AFTALE => (string) new TranslatableMarkup('Aftale', [], ['context' => 'memo action']), + SF1601::ACTION_BEKRAEFT => (string) new TranslatableMarkup('Bekræft', [], ['context' => 'memo action']), + SF1601::ACTION_BETALING => (string) new TranslatableMarkup('Betaling', [], ['context' => 'memo action']), + SF1601::ACTION_FORBEREDELSE => (string) new TranslatableMarkup('Forberedelse', [], ['context' => 'memo action']), + SF1601::ACTION_INFORMATION => (string) new TranslatableMarkup('Information', [], ['context' => 'memo action']), + SF1601::ACTION_SELVBETJENING => (string) new TranslatableMarkup('Selvbetjening', [], ['context' => 'memo action']), + SF1601::ACTION_TILMELDING => (string) new TranslatableMarkup('Tilmelding', [], ['context' => 'memo action']), + SF1601::ACTION_UNDERSKRIV => (string) new TranslatableMarkup('Underskriv', [], ['context' => 'memo action']), + ]; + } + + return self::$translatedActionNames; + } + + /** + * Get translated action name. + */ + public static function getTranslatedActionName(string $action): string { + return self::$translatedActionNames[$action] ?? $action; + } + } diff --git a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php index ccb61a0d..0b1dc219 100644 --- a/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php +++ b/modules/os2forms_digital_post/src/Plugin/WebformHandler/WebformHandlerSF1601.php @@ -3,6 +3,7 @@ namespace Drupal\os2forms_digital_post\Plugin\WebformHandler; use Drupal\Core\Form\FormStateInterface; +use Drupal\os2forms_digital_post\Helper\MeMoHelper; use Drupal\os2forms_digital_post\Helper\WebformHelperSF1601; use Drupal\webform\Plugin\WebformHandlerBase; use Drupal\webform\WebformSubmissionInterface; @@ -136,23 +137,18 @@ public function buildConfigurationForm(array $form, FormStateInterface $formStat '#description' => $this->t('Remove an action by clearing %action and saving.', ['%action' => (string) $this->t('Action')]), ]; + $form[self::MEMO_ACTIONS]['message'] = [ + '#markup' => $this->t('Important: All action URLs must be absolute and secure, i.e. start with https://.'), + ]; + $form[self::MEMO_ACTIONS]['actions'] = [ '#type' => 'table', ]; - $actionOptions = [ - // @todo Handle SF1601::ACTION_AFTALE. - SF1601::ACTION_BEKRAEFT => $this->getTranslatedActionName(SF1601::ACTION_BEKRAEFT), - SF1601::ACTION_BETALING => $this->getTranslatedActionName(SF1601::ACTION_BETALING), - SF1601::ACTION_FORBEREDELSE => $this->getTranslatedActionName(SF1601::ACTION_FORBEREDELSE), - SF1601::ACTION_INFORMATION => $this->getTranslatedActionName(SF1601::ACTION_INFORMATION), - SF1601::ACTION_SELVBETJENING => $this->getTranslatedActionName(SF1601::ACTION_SELVBETJENING), - SF1601::ACTION_TILMELDING => $this->getTranslatedActionName(SF1601::ACTION_TILMELDING), - SF1601::ACTION_UNDERSKRIV => $this->getTranslatedActionName(SF1601::ACTION_UNDERSKRIV), - ]; + $actionOptions = MeMoHelper::getTranslatedActionNames(); $actions = $this->configuration[self::MEMO_ACTIONS]['actions'] ?? []; for ($i = 0; $i <= count($actions); $i++) { - $action = $actions[$i]; + $action = $actions[$i] ?? []; $form[self::MEMO_ACTIONS]['actions'][$i]['action'] = [ '#type' => 'select', '#title' => $this->t('Action'), @@ -269,16 +265,30 @@ public function validateConfigurationForm(array &$form, FormStateInterface $form $formState->setErrorByName( self::MEMO_ACTIONS . '][actions][' . $index . '][url', $this->t('Url for action %action is required.', [ - '%action' => $this->getTranslatedActionName($action['action']), - '%url' => $action['url'] ?? '', + '%action' => MeMoHelper::getTranslatedActionName($action['action']), ]) ); } + else { + $url = $action['url']; + // Add warning if URL contains tokens. + if (preg_match('/\[[a-z_:]+/', $url)) { + $message = $this->t('Make sure that the tokens in the URL @url for action %action expands to an absolute URL, i.e. something starting with https://.', [ + '@url' => $url, + '%action' => MeMoHelper::getTranslatedActionName($action['action']), + ]); + $this->messenger()->addWarning($message); + continue; + } + if ($message = MeMoHelper::validateActionUrl($url, $action)) { + $formState->setErrorByName(self::MEMO_ACTIONS . '][actions][' . $index . '][url', $message); + } + } if (isset($definedActions[$action['action']])) { $formState->setErrorByName( self::MEMO_ACTIONS . '][actions][' . $index . '][action', $this->t('Action %action already defined.', [ - '%action' => $this->getTranslatedActionName($action['action']), + '%action' => MeMoHelper::getTranslatedActionName($action['action']), ]) ); } @@ -337,33 +347,4 @@ public function postPurge(array $webformSubmissions) { $this->helper->deleteMessages($webformSubmissions); } - /** - * Translated action names. - * - * @var array|null - * - * @phpstan-var array - */ - private ?array $translatedActionNames = NULL; - - /** - * Get translated action name. - */ - private function getTranslatedActionName(string $action): string { - if (NULL === $this->translatedActionNames) { - $this->translatedActionNames = [ - SF1601::ACTION_AFTALE => (string) $this->t('Aftale', [], ['context' => 'memo action']), - SF1601::ACTION_BEKRAEFT => (string) $this->t('Bekræft', [], ['context' => 'memo action']), - SF1601::ACTION_BETALING => (string) $this->t('Betaling', [], ['context' => 'memo action']), - SF1601::ACTION_FORBEREDELSE => (string) $this->t('Forberedelse', [], ['context' => 'memo action']), - SF1601::ACTION_INFORMATION => (string) $this->t('Information', [], ['context' => 'memo action']), - SF1601::ACTION_SELVBETJENING => (string) $this->t('Selvbetjening', [], ['context' => 'memo action']), - SF1601::ACTION_TILMELDING => (string) $this->t('Tilmelding', [], ['context' => 'memo action']), - SF1601::ACTION_UNDERSKRIV => (string) $this->t('Underskriv', [], ['context' => 'memo action']), - ]; - } - - return $this->translatedActionNames[$action] ?? $action; - } - } diff --git a/modules/os2forms_forloeb/src/MaestroHelper.php b/modules/os2forms_forloeb/src/MaestroHelper.php index d2dd0c52..42dbd778 100644 --- a/modules/os2forms_forloeb/src/MaestroHelper.php +++ b/modules/os2forms_forloeb/src/MaestroHelper.php @@ -205,7 +205,14 @@ public function processJob(Job $job): JobResult { $submission = $this->webformSubmissionStorage->load($submissionID); - $this->sendNotification($notificationType, $submission, $templateTask, $maestroQueueID); + try { + $this->sendNotification($notificationType, $submission, $templateTask, $maestroQueueID); + } + catch (\Exception $e) { + // Logging is done by the sendNotification method. + // The job should be considered failed. + return JobResult::failure($e->getMessage()); + } return JobResult::success(); } @@ -261,12 +268,15 @@ private function sendNotification( } } catch (\Exception $exception) { + // Log with context and rethrow exception. $this->error('Error sending notification: @message', $context + [ '@message' => $exception->getMessage(), 'handler_id' => 'os2forms_forloeb', 'operation' => 'notification failed', 'exception' => $exception, ]); + + throw $exception; } } @@ -389,7 +399,10 @@ private function sendNotificationDigitalPost( $senderLabel = $subject; $messageLabel = $subject; - $recipientLookupResult = $this->digitalPostHelper->lookupRecipient($recipient); + // Remove all non-digits from recipient identifier. + $recipientIdentifier = preg_replace('/[^\d]+/', '', $recipient); + + $recipientLookupResult = $this->digitalPostHelper->lookupRecipient($recipientIdentifier); $actions = [ (new Action()) ->setActionCode(SF1601::ACTION_SELVBETJENING)