Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ before starting to add changes. Use example [placed in the end of the page](#exa

## [Unreleased]

- [PR-301](https://github.com/OS2Forms/os2forms/pull/301)
Add address information to Digital Post shipments to ensure "*fjernprint*"
can be sent.

## [5.0.0] 2025-11-18

- [PR-192](https://github.com/OS2Forms/os2forms/pull/192)
Expand Down
151 changes: 151 additions & 0 deletions modules/os2forms_digital_post/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,154 @@ of recipients:
``` shell
drush os2forms-digital-post:test:send --help
```

## Fjernprint (physical digital post)

To comply with the address placement in the envelope window (kuvert-rude) an
[event subscriber](src/EventSubscriber/Os2formsDigitalPostSubscriber.php) is
used to inject an address information element into generated HTML before it is
converted to a PDF.

We are only guaranteed to have the necessary information when in a digital
post context. For that reason, the injection of address information is only
done when in a digital post context. Note also that the information is only
injected – it is not styled. This allows flexibility across installations but
also means that it is up to individual installations to style it correctly.
This should be done in OS2Forms Attachment-templates, see
[Overwriting templates](https://github.com/OS2Forms/os2forms/tree/develop/modules/os2forms_attachment#overwriting-templates).

To see the exact requirements for address placement, see
[digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf](docs/digst_a4_farve_ej_til_kant_demo_ny_rudeplacering.pdf).

### The injected HTML

Variations of the injected HTML include extended addresses and c/o.

Without extended address information or c/o:

```html

<div id="envelope-window-digital-post">
<div class="h-card">
<div class="p-name">Jeppe</div>
<div><span class="p-street-address">Test vej HouseNr</span></div>
<div><span class="p-postal-code">2100</span> <span class="p-locality">Copenhagen</span></div>
</div>
</div>
```

With just an extended address:

```html
<div id="envelope-window-digital-post">
<div class="h-card">
<div class="p-name">Jeppe</div>
<div><span class="p-street-address">Test vej HouseNr</span> <span class="p-extended-address">Floor AppartmentNr</span></div>
<div><span class="p-postal-code">2100</span> <span class="p-locality">Copenhagen</span></div>
</div>
</div>
```

With just c/o:

```html

<div id="envelope-window-digital-post">
<div class="h-card">
<div class="p-name">Jeppe</div>
<div class="p-name">c/o Mikkel</div><div><span class="p-street-address">Test vej HouseNr</span></div>
<div><span class="p-postal-code">2100</span> <span class="p-locality">Copenhagen</span></div>
</div>
</div>
```

With extended address information and c/o:

```html
<div id="envelope-window-digital-post">
<div class="h-card">
<div class="p-name">Jeppe</div>
<div class="p-name">c/o Mikkel</div>
<div><span class="p-street-address">Test vej HouseNr</span> <span class="p-extended-address">Floor AppartmentNr</span></div>
<div><span class="p-postal-code">2100</span> <span class="p-locality">Copenhagen</span></div>
</div>
</div>
```



### Styling of the HTML

The following SCSS can be used to style the injected HTML accordingly:

```scss
$margin-top: 25mm;
// There is no exact measurement for margin right in the specifications
$margin-right: 10mm;
$margin-bottom: 20mm;
$margin-left: 17mm;
$page-width: 210mm;
$page-height: 297mm;
$envelope-window-height: 89mm;
$envelope-window-width: 115mm;
$recipient-window-height: 21mm;
$recipient-window-width: 59mm;

@page {
size: A4;
margin: 0;
}

body {
margin-top: $margin-top;
margin-right: $margin-right;
margin-bottom: $margin-bottom;
margin-left: $margin-left;
}

header {
position: fixed;
top: 0;
height: $margin-top;
width: calc($page-width - $margin-left - $margin-right);
font-size: 12px;
}

footer {
position: fixed;
bottom: 0;
height: $margin-bottom;
width: calc($page-width - $margin-left - $margin-right);
font-size: 12px;
}

// Style the envelope window that may be injected by Digital Post.
// Note that top/left is made from the assumption that @page has margin 0.
// @see \Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber:onPrintRender
#envelope-window-digital-post {
position: absolute;
top: $margin-top;
left: $margin-left;
height: $envelope-window-height;
width: $envelope-window-width;
background: white
}

// If envelope window is present, move webform content down
// @see os2forms_digital_post
#envelope-window-digital-post ~ * .webform-entity-print-body {
margin-top: $envelope-window-height;
}

// Style the h-card div
#envelope-window-digital-post > div {
position: absolute;
top: 16mm;
left: 4mm;
font-size: 10px;
height: $recipient-window-height;
width: $recipient-window-width;
}

// More custom styling...
```
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ services:
- "@logger.channel.os2forms_digital_post"
- "@logger.channel.os2forms_digital_post_submission"
- "@Drupal\\os2forms_digital_post\\Helper\\DigitalPostHelper"
- "@Drupal\\os2forms_digital_post\\EventSubscriber\\Os2formsDigitalPostSubscriber"

Drupal\os2forms_digital_post\Helper\SF1461Helper:

Expand All @@ -69,3 +70,9 @@ services:
- '@database'
- '@Drupal\os2forms_digital_post\Helper\MeMoHelper'
- '@logger.channel.os2forms_digital_post'

Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber:
arguments:
- '@session'
tags:
- { name: 'event_subscriber' }
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
<?php

namespace Drupal\os2forms_digital_post\EventSubscriber;

use Drupal\entity_print\Event\PrintEvents;
use Drupal\entity_print\Event\PrintHtmlAlterEvent;
use Drupal\os2web_datalookup\LookupResult\CompanyLookupResult;
use Drupal\os2web_datalookup\LookupResult\CprLookupResult;
use Drupal\webform\WebformSubmissionInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpFoundation\Session\SessionInterface;

/**
* Used to alter the generated PDF to align with digital post requirements.
*/
final class Os2formsDigitalPostSubscriber implements EventSubscriberInterface {

public function __construct(private readonly SessionInterface $session) {
}

/**
* Post render entity_print event.
*
* Injects an envelope-window element containing address information.
*/
public function onPrintRender(PrintHtmlAlterEvent $event): void {
$html = &$event->getHtml();

// Only modify HTML if there is exactly one submission.
if (count($event->getEntities()) === 1) {
$submission = $event->getEntities()[0];
if ($submission instanceof WebformSubmissionInterface) {
// Check whether generation is for digital post.
if ($lookupResult = $this->getDigitalPostContext($submission)) {

// Combine address parts.
$streetAddress = $lookupResult->getStreet();

if ($lookupResult->getHouseNr()) {
$streetAddress .= ' ' . $lookupResult->getHouseNr();
}

$extendedAddress = '';

if ($lookupResult->getFloor()) {
$extendedAddress = $lookupResult->getFloor();
}
if ($lookupResult->getApartmentNr()) {
$extendedAddress .= ' ' . $lookupResult->getApartmentNr();
}

// Generate address HTML.
$addressHtml = '<div id="envelope-window-digital-post"><div class="h-card">';
$addressHtml .= '<div class="p-name">' . htmlspecialchars($lookupResult->getName()) . '</div>';
if ($lookupResult->getCoName()) {
$addressHtml .= '<div class="p-name">c/o ' . htmlspecialchars($lookupResult->getCoName()) . '</div>';
}
$addressHtml .= '<div>';
$addressHtml .= '<span class="p-street-address">' . htmlspecialchars($streetAddress) . '</span>';
if (!empty($extendedAddress)) {
$addressHtml .= ' <span class="p-extended-address">' . htmlspecialchars($extendedAddress) . '</span>';
}
$addressHtml .= '</div>';
$addressHtml .= '<div>';
$addressHtml .= '<span class="p-postal-code">' . htmlspecialchars($lookupResult->getPostalCode()) . '</span>';
$addressHtml .= ' <span class="p-locality">' . htmlspecialchars($lookupResult->getCity()) . '</span>';
$addressHtml .= '</div>';
$addressHtml .= '</div>';
$addressHtml .= '</div>';

// Insert address HTML immediately after body opening tag.
$html = preg_replace('@<body[^>]*>@', '${0}' . $addressHtml, $html);
}
}
}

}

/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
PrintEvents::POST_RENDER => ['onPrintRender'],
];
}

/**
* Indicate Digital Post context in the current session.
*/
public function setDigitalPostContext(WebformSubmissionInterface $submission, CompanyLookupResult|CprLookupResult $lookupResult): void {
$key = $this->createSessionKeyFromSubmission($submission);
$this->session->set($key, $lookupResult);
}

/**
* Check for Digital Post context in the current session.
*/
public function getDigitalPostContext(WebformSubmissionInterface $submission): CompanyLookupResult|CprLookupResult|null {
$key = $this->createSessionKeyFromSubmission($submission);

$digitalPostContext = $this->session->get($key);

// We only need/use it once, so just remove it after fetching it.
if ($digitalPostContext) {
$this->session->remove($key);
}

return $digitalPostContext;
}

/**
* Create a session key from a submission that is unique to the submission.
*/
public function createSessionKeyFromSubmission(WebformSubmissionInterface $submission): string {
// Due to cloning of submission during attachment logic, we cannot use
// submission id or uuid. Webform serial, however, is copied along, so a
// combination of webform id and serial is used for uniqueness.
// @see \Drupal\os2forms_attachment\Element\AttachmentElement::overrideWebformSettings
return 'digital_post_context_' . $submission->getWebform()->id() . '_' . $submission->serial();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

namespace Drupal\os2forms_digital_post\Helper;

use Drupal\os2web_datalookup\LookupResult\CompanyLookupResult;
use Drupal\os2web_datalookup\LookupResult\CprLookupResult;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\advancedqueue\Entity\QueueInterface;
use Drupal\advancedqueue\Job;
use Drupal\advancedqueue\JobResult;
use Drupal\os2forms_digital_post\EventSubscriber\Os2formsDigitalPostSubscriber;
use Drupal\os2forms_digital_post\Exception\InvalidRecipientIdentifierElementException;
use Drupal\os2forms_digital_post\Exception\RuntimeException;
use Drupal\os2forms_digital_post\Exception\SubmissionNotFoundException;
Expand Down Expand Up @@ -62,6 +65,7 @@ public function __construct(
#[Autowire(service: 'logger.channel.os2forms_digital_post_submission')]
private readonly LoggerChannelInterface $submissionLogger,
private readonly DigitalPostHelper $digitalPostHelper,
private readonly Os2formsDigitalPostSubscriber $digitalPostSubscriber,
) {
$this->webformSubmissionStorage = $entityTypeManager->getStorage('webform_submission');
$this->queueStorage = $entityTypeManager->getStorage('advancedqueue_queue');
Expand Down Expand Up @@ -152,6 +156,8 @@ public function sendDigitalPost(WebformSubmissionInterface $submission, array $h
$recipientIdentifierType = 'CPR';
}

$this->digitalPostSubscriber->setDigitalPostContext($submission, $lookupResult);

$senderSettings = $this->settings->getSender();
$messageOptions = [
self::RECIPIENT_IDENTIFIER_TYPE => $recipientIdentifierType,
Expand Down
Loading