feat: send accepted teamer invoices to configurable email inbox
This commit is contained in:
@@ -68,5 +68,8 @@ XML_EXPORT_PATH="%kernel.project_dir%/var/xmlexport"
|
||||
BIN_GS=/usr/bin/gs
|
||||
BIN_CONVERT=/usr/bin/convert
|
||||
|
||||
DATEV_EMAIL_RECIPIENT=[email protected]
|
||||
DATEV_EMAIL_SENDER=[email protected]
|
||||
|
||||
FEATURE_SANITIZE_UPLOADS=false
|
||||
FEATURE_STAMP_INVOICES=false
|
||||
|
||||
@@ -7,3 +7,6 @@ flagception:
|
||||
stamp_invoices:
|
||||
default: false
|
||||
env: FEATURE_STAMP_INVOICES
|
||||
datev_email:
|
||||
default: false
|
||||
env: FEATURE_DATEV_EMAIL
|
||||
|
||||
@@ -153,6 +153,11 @@ services:
|
||||
event: oneup_uploader.validation
|
||||
method: onValidate
|
||||
|
||||
App\EventListener\DatevEmailSubscriber:
|
||||
arguments:
|
||||
$datevEmailRecipient: '%env(DATEV_EMAIL_RECIPIENT)%'
|
||||
$datevEmailSender: '%env(DATEV_EMAIL_SENDER)%'
|
||||
|
||||
app.upload_namer:
|
||||
class: App\Service\Upload\UploadNamer
|
||||
public: true
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
# Plan: Cancellation Statistics Feature
|
||||
|
||||
## Objective
|
||||
|
||||
Enable stakeholders to see how often dispositions and applications have been canceled by either the teamer or the office, per hotel and for a given date range.
|
||||
|
||||
---
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### Dispositions - Already Tracked
|
||||
|
||||
The `Disposition` entity has comprehensive cancellation tracking:
|
||||
|
||||
| Field | Type | Purpose |
|
||||
|-------|------|---------|
|
||||
| `status` | string | `'called_off'` when cancelled |
|
||||
| `calledOffBy` | string (nullable) | `'teamer'` or `'office'` |
|
||||
| `calledOffReason` | text (nullable) | Reason for cancellation |
|
||||
| `updatedAt` | datetime | Timestamp of last change |
|
||||
|
||||
**Constants:**
|
||||
```php
|
||||
public const STATUS_CALLED_OFF = 'called_off';
|
||||
public const CALLED_OFF_BY_TEAMER = 'teamer';
|
||||
public const CALLED_OFF_BY_OFFICE = 'office';
|
||||
```
|
||||
|
||||
**Cancellation entry points:**
|
||||
1. Individual disposition cancellation via `Administrative/Disposition/CallOffController`
|
||||
- Form requires `calledOffBy` and `calledOffReason`
|
||||
- Dispatches `DispositionCalledOffEvent`
|
||||
|
||||
2. Full assignment cancellation via `Administrative/Assignment/CallOffController`
|
||||
- Sets all dispositions to `calledOffBy = 'office'`
|
||||
- Dispatches `AssignmentCalledOffEvent`
|
||||
|
||||
### Applications - Missing Tracking
|
||||
|
||||
The `Application` entity lacks cancellation tracking:
|
||||
|
||||
| Current Field | Issue |
|
||||
|---------------|-------|
|
||||
| `status` | Only `'rejected'` - no distinction between teamer withdrawal and office rejection |
|
||||
| - | No `cancelledBy` field |
|
||||
| - | No `cancellationReason` field |
|
||||
|
||||
**Current rejection mechanisms:**
|
||||
1. Manual rejection by office (no tracking of who)
|
||||
2. Automatic rejection by `RejectApplicationListener` when disposition slots fill
|
||||
3. Automatic removal by `InvalidateApplicationsListener` when teamer has overlapping confirmed dispositions
|
||||
|
||||
---
|
||||
|
||||
## Pending Decision
|
||||
|
||||
**Question for stakeholders:** Should automatic rejections be included in statistics?
|
||||
|
||||
| Option | Description | Impact |
|
||||
|--------|-------------|--------|
|
||||
| **Only user-initiated** | Count only manual cancellations by teamer or office | Simpler model, clearer accountability |
|
||||
| **Include automatic** | Also track system-initiated rejections | Full picture of lost applications, requires `'system'` category |
|
||||
|
||||
**Status:** Awaiting stakeholder response
|
||||
|
||||
---
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### Application Entity
|
||||
|
||||
**Option A: If only user-initiated cancellations count**
|
||||
|
||||
Add new status and fields:
|
||||
```php
|
||||
public const STATUS_WITHDRAWN = 'withdrawn'; // teamer-initiated
|
||||
public const STATUS_REJECTED = 'rejected'; // office-initiated (existing)
|
||||
|
||||
private ?string $cancellationReason = null;
|
||||
```
|
||||
|
||||
**Option B: If automatic rejections should be tracked**
|
||||
|
||||
Add new status, fields, and constant:
|
||||
```php
|
||||
public const STATUS_WITHDRAWN = 'withdrawn'; // teamer-initiated
|
||||
public const STATUS_REJECTED = 'rejected'; // office or system initiated
|
||||
|
||||
public const CANCELLED_BY_TEAMER = 'teamer';
|
||||
public const CANCELLED_BY_OFFICE = 'office';
|
||||
public const CANCELLED_BY_SYSTEM = 'system';
|
||||
|
||||
private ?string $cancelledBy = null;
|
||||
private ?string $cancellationReason = null;
|
||||
```
|
||||
|
||||
### Database Migration
|
||||
|
||||
Add columns to `application` table:
|
||||
- `cancellation_reason` (LONGTEXT, nullable)
|
||||
- Possibly `cancelled_by` (VARCHAR(64), nullable) if Option B
|
||||
|
||||
### Repository Methods
|
||||
|
||||
Add to `ApplicationRepository`:
|
||||
```php
|
||||
/**
|
||||
* @return array<string, array{teamer: int, office: int}>
|
||||
*/
|
||||
public function getCancellationStatsByHotel(
|
||||
\DateTimeImmutable $startDate,
|
||||
\DateTimeImmutable $endDate
|
||||
): array;
|
||||
```
|
||||
|
||||
Add to `DispositionRepository`:
|
||||
```php
|
||||
/**
|
||||
* @return array<string, array{teamer: int, office: int}>
|
||||
*/
|
||||
public function getCancellationStatsByHotel(
|
||||
\DateTimeImmutable $startDate,
|
||||
\DateTimeImmutable $endDate
|
||||
): array;
|
||||
```
|
||||
|
||||
**Date filtering:** Based on assignment travel dates (confirmed in discussion).
|
||||
|
||||
### Update Listeners (if Option B)
|
||||
|
||||
Modify `RejectApplicationListener` and `InvalidateApplicationsListener` to set `cancelledBy = 'system'`.
|
||||
|
||||
### UI Changes
|
||||
|
||||
Add withdrawal functionality for teamers (if not already present) that sets `STATUS_WITHDRAWN`.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Steps
|
||||
|
||||
1. [ ] Finalize decision on automatic rejection tracking
|
||||
2. [ ] Add new constants and fields to `Application` entity
|
||||
3. [ ] Create database migration
|
||||
4. [ ] Update `ApplicationRepository` with statistics methods
|
||||
5. [ ] Update `DispositionRepository` with statistics methods
|
||||
6. [ ] Update listeners if tracking automatic rejections
|
||||
7. [ ] Add/update controllers for teamer withdrawal flow
|
||||
8. [ ] Write tests for new repository methods
|
||||
9. [ ] Run php-cs-fixer
|
||||
|
||||
---
|
||||
|
||||
## Related Files
|
||||
|
||||
### Entities
|
||||
- `src/Entity/Application.php`
|
||||
- `src/Entity/Disposition.php`
|
||||
- `src/Entity/Assignment.php`
|
||||
|
||||
### Repositories
|
||||
- `src/Repository/ApplicationRepository.php`
|
||||
- `src/Repository/DispositionRepository.php`
|
||||
|
||||
### Controllers
|
||||
- `src/Controller/Administrative/Disposition/CallOffController.php`
|
||||
- `src/Controller/Administrative/Assignment/CallOffController.php`
|
||||
- `src/Controller/Teamer/Disposition/DetailController.php`
|
||||
|
||||
### Listeners
|
||||
- `src/EventListener/InvalidateApplicationsListener.php`
|
||||
- `src/EventListener/RejectApplicationListener.php`
|
||||
|
||||
### Events
|
||||
- `src/Event/DispositionCalledOffEvent.php`
|
||||
- `src/Event/AssignmentCalledOffEvent.php`
|
||||
|
||||
### Forms
|
||||
- `src/Form/DispositionCallOffType.php`
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
- Filter statistics by **assignment travel dates**, not cancellation timestamp
|
||||
- Disposition tracking already works - only need repository query methods
|
||||
- Application tracking requires entity changes and migration
|
||||
- Consider whether teamer self-service withdrawal exists or needs to be added
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Email;
|
||||
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
|
||||
interface EmailAttachmentInterface
|
||||
{
|
||||
public function attachTo(TemplatedEmail $email): void;
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Email;
|
||||
|
||||
use App\Model\EmailAttachmentDto;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
|
||||
@@ -46,8 +45,8 @@ class Mailer
|
||||
;
|
||||
|
||||
foreach ($config['attachments'] as $attachment) {
|
||||
/* @var EmailAttachmentDto $attachment */
|
||||
$email->attach($attachment->getContent(), $attachment->getName(), $attachment->getMimeType());
|
||||
/* @var EmailAttachmentInterface $attachment */
|
||||
$attachment->attachTo($email);
|
||||
}
|
||||
|
||||
$this->bodyRenderer->render($email);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\EventListener;
|
||||
|
||||
use App\Email\Mailer;
|
||||
use App\Entity\Upload;
|
||||
use App\Event\DocumentConfirmedEvent;
|
||||
use App\Model\EmailPathAttachmentDto;
|
||||
use App\Service\Upload\DownloadNamer;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
use Flagception\Manager\FeatureManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
class DatevEmailSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly Mailer $mailer,
|
||||
private readonly UploadHandler $uploadHandler,
|
||||
private readonly LoggerInterface $logger,
|
||||
private readonly string $datevEmailRecipient,
|
||||
private readonly string $datevEmailSender,
|
||||
private readonly FeatureManagerInterface $featureManager,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
DocumentConfirmedEvent::NAME => 'onDocumentConfirmed',
|
||||
];
|
||||
}
|
||||
|
||||
public function onDocumentConfirmed(DocumentConfirmedEvent $event): void
|
||||
{
|
||||
if (false === $this->featureManager->isActive('datev_email')) {
|
||||
return;
|
||||
}
|
||||
|
||||
$document = $event->getDocument();
|
||||
|
||||
if (Upload::TYPE_INVOICE !== $document->getType()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$disposition = $document->getDisposition();
|
||||
|
||||
if (null === $disposition) {
|
||||
$this->logger->warning('DATEV email skipped: document has no disposition', [
|
||||
'document' => $document->getId(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$teamer = $disposition->getTeamer();
|
||||
|
||||
if (null === $teamer) {
|
||||
$this->logger->warning('DATEV email skipped: disposition has no teamer', [
|
||||
'document' => $document->getId(),
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$path = $this->uploadHandler->getUploadFilepath($document);
|
||||
|
||||
if (false === file_exists($path)) {
|
||||
$this->logger->error('DATEV email skipped: attachment file not found', [
|
||||
'document' => $document->getId(),
|
||||
'path' => $path,
|
||||
]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$filename = (new DownloadNamer())->nameForTeamer($document, $teamer);
|
||||
$attachment = new EmailPathAttachmentDto($path, $filename);
|
||||
|
||||
$this->mailer->createAndSendEmail([
|
||||
'disposition' => $disposition,
|
||||
], [
|
||||
'to' => $this->datevEmailRecipient,
|
||||
'from' => $this->datevEmailSender,
|
||||
'subject' => 'E&P Team Honorarnote '.$teamer,
|
||||
'template' => 'email/datev_invoice.html.twig',
|
||||
'attachments' => [
|
||||
$attachment,
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
class EmailAttachmentDto
|
||||
use App\Email\EmailAttachmentInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
|
||||
class EmailAttachmentDto implements EmailAttachmentInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $name,
|
||||
@@ -25,4 +30,9 @@ class EmailAttachmentDto
|
||||
{
|
||||
return $this->mimeType;
|
||||
}
|
||||
|
||||
public function attachTo(TemplatedEmail $email): void
|
||||
{
|
||||
$email->attach($this->content, $this->name, $this->mimeType);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Model;
|
||||
|
||||
use App\Email\EmailAttachmentInterface;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
|
||||
class EmailPathAttachmentDto implements EmailAttachmentInterface
|
||||
{
|
||||
public function __construct(
|
||||
private readonly string $path,
|
||||
private readonly string $name,
|
||||
private readonly string $mimeType = 'application/pdf',
|
||||
) {
|
||||
}
|
||||
|
||||
public function getPath(): string
|
||||
{
|
||||
return $this->path;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function getMimeType(): string
|
||||
{
|
||||
return $this->mimeType;
|
||||
}
|
||||
|
||||
public function attachTo(TemplatedEmail $email): void
|
||||
{
|
||||
$email->attachFromPath($this->path, $this->name, $this->mimeType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{% extends 'email/layout.html.twig' %}
|
||||
|
||||
{% block body %}
|
||||
<h1>
|
||||
Honorarnote
|
||||
</h1>
|
||||
<p>
|
||||
Die angehangene Honorarnote von {{ disposition.teamer }} zum Einsatz {{ disposition.assignment.destination }}
|
||||
wurde freigegeben.
|
||||
</p>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,190 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\EventListener;
|
||||
|
||||
use App\Email\Mailer;
|
||||
use App\Entity\Disposition;
|
||||
use App\Entity\Teamer;
|
||||
use App\Entity\Upload;
|
||||
use App\Event\DocumentConfirmedEvent;
|
||||
use App\EventListener\DatevEmailSubscriber;
|
||||
use App\Model\EmailPathAttachmentDto;
|
||||
use App\Service\Upload\UploadHandler;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class DatevEmailSubscriberTest extends TestCase
|
||||
{
|
||||
private Mailer&MockObject $mailer;
|
||||
private UploadHandler&MockObject $uploadHandler;
|
||||
private LoggerInterface&MockObject $logger;
|
||||
private DatevEmailSubscriber $subscriber;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->mailer = $this->createMock(Mailer::class);
|
||||
$this->uploadHandler = $this->createMock(UploadHandler::class);
|
||||
$this->logger = $this->createMock(LoggerInterface::class);
|
||||
|
||||
$this->subscriber = new DatevEmailSubscriber(
|
||||
$this->mailer,
|
||||
$this->uploadHandler,
|
||||
$this->logger,
|
||||
'[email protected]',
|
||||
'[email protected]',
|
||||
);
|
||||
}
|
||||
|
||||
public function testGetSubscribedEvents(): void
|
||||
{
|
||||
$events = DatevEmailSubscriber::getSubscribedEvents();
|
||||
|
||||
$this->assertArrayHasKey(DocumentConfirmedEvent::NAME, $events);
|
||||
$this->assertSame('onDocumentConfirmed', $events[DocumentConfirmedEvent::NAME]);
|
||||
}
|
||||
|
||||
public function testSkipsNonInvoiceDocuments(): void
|
||||
{
|
||||
$document = $this->createMock(Upload::class);
|
||||
$document->method('getType')->willReturn(Upload::TYPE_CONTRACT);
|
||||
|
||||
$this->mailer
|
||||
->expects($this->never())
|
||||
->method('createAndSendEmail');
|
||||
|
||||
$event = new DocumentConfirmedEvent($document);
|
||||
$this->subscriber->onDocumentConfirmed($event);
|
||||
}
|
||||
|
||||
public function testSkipsDocumentWithoutDisposition(): void
|
||||
{
|
||||
$document = $this->createMock(Upload::class);
|
||||
$document->method('getType')->willReturn(Upload::TYPE_INVOICE);
|
||||
$document->method('getDisposition')->willReturn(null);
|
||||
$document->method('getId')->willReturn(1);
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('warning')
|
||||
->with('DATEV email skipped: document has no disposition', ['document' => 1]);
|
||||
|
||||
$this->mailer
|
||||
->expects($this->never())
|
||||
->method('createAndSendEmail');
|
||||
|
||||
$event = new DocumentConfirmedEvent($document);
|
||||
$this->subscriber->onDocumentConfirmed($event);
|
||||
}
|
||||
|
||||
public function testSkipsDispositionWithoutTeamer(): void
|
||||
{
|
||||
$disposition = $this->createMock(Disposition::class);
|
||||
$disposition->method('getTeamer')->willReturn(null);
|
||||
|
||||
$document = $this->createMock(Upload::class);
|
||||
$document->method('getType')->willReturn(Upload::TYPE_INVOICE);
|
||||
$document->method('getDisposition')->willReturn($disposition);
|
||||
$document->method('getId')->willReturn(2);
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('warning')
|
||||
->with('DATEV email skipped: disposition has no teamer', ['document' => 2]);
|
||||
|
||||
$this->mailer
|
||||
->expects($this->never())
|
||||
->method('createAndSendEmail');
|
||||
|
||||
$event = new DocumentConfirmedEvent($document);
|
||||
$this->subscriber->onDocumentConfirmed($event);
|
||||
}
|
||||
|
||||
public function testSkipsMissingAttachmentFile(): void
|
||||
{
|
||||
$teamer = $this->createMock(Teamer::class);
|
||||
$teamer->method('getFullName')->willReturn('Doe, John');
|
||||
|
||||
$disposition = $this->createMock(Disposition::class);
|
||||
$disposition->method('getTeamer')->willReturn($teamer);
|
||||
|
||||
$document = $this->createMock(Upload::class);
|
||||
$document->method('getType')->willReturn(Upload::TYPE_INVOICE);
|
||||
$document->method('getDisposition')->willReturn($disposition);
|
||||
$document->method('getId')->willReturn(3);
|
||||
|
||||
$nonExistentPath = '/tmp/non_existent_file_'.uniqid().'.pdf';
|
||||
|
||||
$this->uploadHandler
|
||||
->method('getUploadFilepath')
|
||||
->with($document)
|
||||
->willReturn($nonExistentPath);
|
||||
|
||||
$this->logger
|
||||
->expects($this->once())
|
||||
->method('error')
|
||||
->with('DATEV email skipped: attachment file not found', [
|
||||
'document' => 3,
|
||||
'path' => $nonExistentPath,
|
||||
]);
|
||||
|
||||
$this->mailer
|
||||
->expects($this->never())
|
||||
->method('createAndSendEmail');
|
||||
|
||||
$event = new DocumentConfirmedEvent($document);
|
||||
$this->subscriber->onDocumentConfirmed($event);
|
||||
}
|
||||
|
||||
public function testSendsEmailWithAttachment(): void
|
||||
{
|
||||
$teamer = $this->createMock(Teamer::class);
|
||||
$teamer->method('getFullName')->willReturn('Doe, John');
|
||||
$teamer->method('__toString')->willReturn('Doe, John');
|
||||
|
||||
$disposition = $this->createMock(Disposition::class);
|
||||
$disposition->method('getTeamer')->willReturn($teamer);
|
||||
|
||||
$document = $this->createMock(Upload::class);
|
||||
$document->method('getDisposition')->willReturn($disposition);
|
||||
$document->method('getType')->willReturn(Upload::TYPE_INVOICE);
|
||||
$document->method('getId')->willReturn(42);
|
||||
$document->method('getOriginalFilename')->willReturn('original.pdf');
|
||||
$document->method('getCreatedAt')->willReturn(new \DateTimeImmutable('2026-01-15'));
|
||||
$document->method('getFilename')->willReturn('abc123.pdf');
|
||||
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'datev_test_');
|
||||
|
||||
try {
|
||||
$this->uploadHandler
|
||||
->expects($this->once())
|
||||
->method('getUploadFilepath')
|
||||
->with($document)
|
||||
->willReturn($tmpFile);
|
||||
|
||||
$this->mailer
|
||||
->expects($this->once())
|
||||
->method('createAndSendEmail')
|
||||
->willReturnCallback(function (array $context, array $options): void {
|
||||
$this->assertSame('[email protected]', $options['to']);
|
||||
$this->assertSame('[email protected]', $options['from']);
|
||||
$this->assertSame('email/datev_invoice.html.twig', $options['template']);
|
||||
$this->assertStringContainsString('Honorarnote', $options['subject']);
|
||||
|
||||
$this->assertArrayHasKey('attachments', $options);
|
||||
$this->assertCount(1, $options['attachments']);
|
||||
|
||||
$attachment = $options['attachments'][0];
|
||||
$this->assertInstanceOf(EmailPathAttachmentDto::class, $attachment);
|
||||
$this->assertSame('hn-42-doe_john-20260115.pdf', $attachment->getName());
|
||||
});
|
||||
|
||||
$event = new DocumentConfirmedEvent($document);
|
||||
$this->subscriber->onDocumentConfirmed($event);
|
||||
} finally {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Model;
|
||||
|
||||
use App\Model\EmailAttachmentDto;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
|
||||
class EmailAttachmentDtoTest extends TestCase
|
||||
{
|
||||
public function testAttachToAddsContentAttachment(): void
|
||||
{
|
||||
$dto = new EmailAttachmentDto('invoice.pdf', 'pdf-content', 'application/pdf');
|
||||
$email = new TemplatedEmail();
|
||||
|
||||
$dto->attachTo($email);
|
||||
|
||||
$attachments = $email->getAttachments();
|
||||
$this->assertCount(1, $attachments);
|
||||
|
||||
$body = $attachments[0]->getBody();
|
||||
$this->assertSame('pdf-content', $body);
|
||||
$this->assertSame('invoice.pdf', $attachments[0]->getFilename());
|
||||
$this->assertSame('application/pdf', $attachments[0]->getMediaType().'/'.$attachments[0]->getMediaSubtype());
|
||||
}
|
||||
|
||||
public function testGettersReturnConstructorValues(): void
|
||||
{
|
||||
$dto = new EmailAttachmentDto('test.csv', 'csv-data', 'text/csv');
|
||||
|
||||
$this->assertSame('test.csv', $dto->getName());
|
||||
$this->assertSame('csv-data', $dto->getContent());
|
||||
$this->assertSame('text/csv', $dto->getMimeType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Model;
|
||||
|
||||
use App\Model\EmailPathAttachmentDto;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
|
||||
|
||||
class EmailPathAttachmentDtoTest extends TestCase
|
||||
{
|
||||
public function testAttachToAddsPathAttachment(): void
|
||||
{
|
||||
$tmpFile = tempnam(sys_get_temp_dir(), 'test_attachment_');
|
||||
file_put_contents($tmpFile, 'dummy-pdf-content');
|
||||
|
||||
try {
|
||||
$dto = new EmailPathAttachmentDto($tmpFile, 'invoice.pdf');
|
||||
$email = new TemplatedEmail();
|
||||
|
||||
$dto->attachTo($email);
|
||||
|
||||
$attachments = $email->getAttachments();
|
||||
$this->assertCount(1, $attachments);
|
||||
$this->assertSame('invoice.pdf', $attachments[0]->getFilename());
|
||||
$this->assertSame('application/pdf', $attachments[0]->getMediaType().'/'.$attachments[0]->getMediaSubtype());
|
||||
} finally {
|
||||
unlink($tmpFile);
|
||||
}
|
||||
}
|
||||
|
||||
public function testDefaultMimeTypeIsApplicationPdf(): void
|
||||
{
|
||||
$dto = new EmailPathAttachmentDto('/tmp/test.pdf', 'test.pdf');
|
||||
|
||||
$this->assertSame('application/pdf', $dto->getMimeType());
|
||||
}
|
||||
|
||||
public function testCustomMimeType(): void
|
||||
{
|
||||
$dto = new EmailPathAttachmentDto('/tmp/test.csv', 'test.csv', 'text/csv');
|
||||
|
||||
$this->assertSame('text/csv', $dto->getMimeType());
|
||||
}
|
||||
|
||||
public function testGettersReturnConstructorValues(): void
|
||||
{
|
||||
$dto = new EmailPathAttachmentDto('/var/uploads/file.pdf', 'document.pdf', 'application/pdf');
|
||||
|
||||
$this->assertSame('/var/uploads/file.pdf', $dto->getPath());
|
||||
$this->assertSame('document.pdf', $dto->getName());
|
||||
$this->assertSame('application/pdf', $dto->getMimeType());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user