feat: dummy replacement pdf upload for environments other than prod

This commit is contained in:
Björn Fromme
2026-03-15 12:42:28 +01:00
parent 3b99757a08
commit 8988ae7620
12 changed files with 78 additions and 16 deletions
+2
View File
@@ -73,3 +73,5 @@ [email protected]
FEATURE_SANITIZE_UPLOADS=false FEATURE_SANITIZE_UPLOADS=false
FEATURE_STAMP_INVOICES=false FEATURE_STAMP_INVOICES=false
UPLOAD_REPLACEMENT_FILE=assets/pdf/chicken.pdf
Binary file not shown.
+2
View File
@@ -135,6 +135,8 @@ services:
arguments: arguments:
$orphanageManager: '@oneup_uploader.orphanage_manager' $orphanageManager: '@oneup_uploader.orphanage_manager'
$projectDir: '%kernel.project_dir%' $projectDir: '%kernel.project_dir%'
$environment: '%kernel.environment%'
$uploadReplacementFile: '%env(default::UPLOAD_REPLACEMENT_FILE)%'
App\Service\Pdf\Sanitizer: App\Service\Pdf\Sanitizer:
arguments: arguments:
@@ -64,7 +64,7 @@ class BatchDownloadController extends AbstractController
/** @var Upload $upload */ /** @var Upload $upload */
$teamer = $upload->getOwner()->getTeamer(); $teamer = $upload->getOwner()->getTeamer();
$filename = $namer->nameForTeamer($upload, $teamer); $filename = $namer->nameForTeamer($upload, $teamer);
$filepath = $this->uploadHandler->getUploadFilepath($upload); $filepath = $this->uploadHandler->getResolvedUploadFilepath($upload);
try { try {
$zip->addFileFromPath(fileName: $filename, path: $filepath); $zip->addFileFromPath(fileName: $filename, path: $filepath);
+1 -1
View File
@@ -29,7 +29,7 @@ class DownloadController extends AbstractController
#[IsGranted('DOWNLOAD', subject: 'upload')] #[IsGranted('DOWNLOAD', subject: 'upload')]
public function index(Upload $upload, Request $request): Response public function index(Upload $upload, Request $request): Response
{ {
$path = $this->uploadHandler->getUploadFilepath($upload); $path = $this->uploadHandler->getResolvedUploadFilepath($upload);
if (true === in_array($upload->getType(), [Upload::TYPE_INVOICE, Upload::TYPE_CONTRACT])) { if (true === in_array($upload->getType(), [Upload::TYPE_INVOICE, Upload::TYPE_CONTRACT])) {
$namer = new DownloadNamer(); $namer = new DownloadNamer();
+1 -1
View File
@@ -65,7 +65,7 @@ class DatevEmailSubscriber implements EventSubscriberInterface
return; return;
} }
$path = $this->uploadHandler->getUploadFilepath($document); $path = $this->uploadHandler->getResolvedUploadFilepath($document);
if (false === file_exists($path)) { if (false === file_exists($path)) {
$this->logger->error('DATEV email skipped: attachment file not found', [ $this->logger->error('DATEV email skipped: attachment file not found', [
+25 -9
View File
@@ -4,10 +4,20 @@ namespace App\Service\Pdf;
use App\Entity\Disposition; use App\Entity\Disposition;
use App\Entity\Upload; use App\Entity\Upload;
use App\Service\Upload\UploadHandler;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use Symfony\Contracts\Translation\TranslatorInterface;
class InvoiceApprover extends AbstractPdfRenderer class InvoiceApprover extends AbstractPdfRenderer
{ {
public function __construct(
TranslatorInterface $translator,
array $options,
private readonly UploadHandler $uploadHandler,
) {
parent::__construct($translator, $options);
}
/** /**
* @throws \InvalidArgumentException * @throws \InvalidArgumentException
*/ */
@@ -16,24 +26,23 @@ class InvoiceApprover extends AbstractPdfRenderer
$teamer = $disposition->getTeamer(); $teamer = $disposition->getTeamer();
$assignment = $disposition->getAssignment(); $assignment = $disposition->getAssignment();
$invoice = $disposition->getDocumentByType(Upload::TYPE_INVOICE); $invoice = $disposition->getDocumentByType(Upload::TYPE_INVOICE);
$originalFilename = $invoice->getFilename();
if ('application/pdf' !== $invoice->getMimeType()) { if ('application/pdf' !== $invoice->getMimeType()) {
throw new \InvalidArgumentException('Expected PDF but got '.$invoice->getMimeType()); throw new \InvalidArgumentException('Expected PDF but got '.$invoice->getMimeType());
} }
// Load template $sourcePath = $this->uploadHandler->getResolvedUploadFilepath($invoice);
$folder = substr($originalFilename, 0, 1); $targetPath = $this->uploadHandler->getUploadFilepath($invoice);
$filepath = sprintf('%s/uploads/invoice/%s/%s', $this->config['project_dir'], $folder, $originalFilename);
if (false === file_exists($filepath)) { if (false === file_exists($sourcePath)) {
throw new \InvalidArgumentException('Uploaded document not found at '.$filepath); throw new \InvalidArgumentException('Uploaded document not found at '.$sourcePath);
} }
$pdf = new Pdf(); $pdf = new Pdf();
$pdf->SetAuthor($teamer->getFullName(), true); $pdf->SetAuthor($teamer->getFullName(), true);
$pdf->SetCreator($teamer->getFullName(), true); $pdf->SetCreator($teamer->getFullName(), true);
$pdf->appendPdf($filepath); $pdf->appendPdf($sourcePath);
$pdf->setPage(1); $pdf->setPage(1);
// Cost unit job profile // Cost unit job profile
@@ -53,9 +62,16 @@ class InvoiceApprover extends AbstractPdfRenderer
$pdf->SetTextColor(0, 128, 0); $pdf->SetTextColor(0, 128, 0);
$pdf->Text(20, 30, $pdf->encodeString('geprüft: '.$invoice->getApprovedBy())); $pdf->Text(20, 30, $pdf->encodeString('geprüft: '.$invoice->getApprovedBy()));
$this->createBackup($filepath); $filesystem = new Filesystem();
$targetDir = \dirname($targetPath);
$pdf->Output('F', $filepath); if (false === $filesystem->exists($targetDir)) {
$filesystem->mkdir($targetDir);
}
$this->createBackup($targetPath);
$pdf->Output('F', $targetPath);
return $pdf; return $pdf;
} }
+23
View File
@@ -22,6 +22,8 @@ class UploadHandler
private readonly Sanitizer $sanitizer, private readonly Sanitizer $sanitizer,
private readonly LoggerInterface $logger, private readonly LoggerInterface $logger,
private readonly string $projectDir, private readonly string $projectDir,
private readonly string $environment,
private readonly string $uploadReplacementFile,
) { ) {
} }
@@ -143,6 +145,27 @@ class UploadHandler
return true === $absolute ? sprintf('%s/%s', $this->projectDir, $relativePath) : $relativePath; return true === $absolute ? sprintf('%s/%s', $this->projectDir, $relativePath) : $relativePath;
} }
/**
* Returns the upload file path, substituting a replacement file in non-production environments
* when configured. Use this for read-only access (downloads, email attachments).
*/
public function getResolvedUploadFilepath(Upload $upload): string
{
$originalPath = $this->getUploadFilepath($upload);
if ('' === $this->uploadReplacementFile || 'prod' === $this->environment) {
return $originalPath;
}
$replacementPath = sprintf('%s/%s', $this->projectDir, $this->uploadReplacementFile);
if (false === file_exists($replacementPath)) {
return $originalPath;
}
return $replacementPath;
}
public function getTempUploadFilepath(UploadDto $uploadDto, string $uploadSessionId): string public function getTempUploadFilepath(UploadDto $uploadDto, string $uploadSessionId): string
{ {
$tempUploadDir = $this->getTempUploadDir($uploadSessionId); $tempUploadDir = $this->getTempUploadDir($uploadSessionId);
@@ -12,6 +12,7 @@ use App\Event\DocumentConfirmedEvent;
use App\EventListener\DatevEmailSubscriber; use App\EventListener\DatevEmailSubscriber;
use App\Model\EmailPathAttachmentDto; use App\Model\EmailPathAttachmentDto;
use App\Service\Upload\UploadHandler; use App\Service\Upload\UploadHandler;
use Flagception\Manager\FeatureManagerInterface;
use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
@@ -21,6 +22,7 @@ class DatevEmailSubscriberTest extends TestCase
private Mailer&MockObject $mailer; private Mailer&MockObject $mailer;
private UploadHandler&MockObject $uploadHandler; private UploadHandler&MockObject $uploadHandler;
private LoggerInterface&MockObject $logger; private LoggerInterface&MockObject $logger;
private FeatureManagerInterface&MockObject $featureManager;
private DatevEmailSubscriber $subscriber; private DatevEmailSubscriber $subscriber;
protected function setUp(): void protected function setUp(): void
@@ -28,6 +30,8 @@ class DatevEmailSubscriberTest extends TestCase
$this->mailer = $this->createMock(Mailer::class); $this->mailer = $this->createMock(Mailer::class);
$this->uploadHandler = $this->createMock(UploadHandler::class); $this->uploadHandler = $this->createMock(UploadHandler::class);
$this->logger = $this->createMock(LoggerInterface::class); $this->logger = $this->createMock(LoggerInterface::class);
$this->featureManager = $this->createMock(FeatureManagerInterface::class);
$this->featureManager->method('isActive')->with('datev_email')->willReturn(true);
$this->subscriber = new DatevEmailSubscriber( $this->subscriber = new DatevEmailSubscriber(
$this->mailer, $this->mailer,
@@ -35,6 +39,7 @@ class DatevEmailSubscriberTest extends TestCase
$this->logger, $this->logger,
'[email protected]', '[email protected]',
'[email protected]', '[email protected]',
$this->featureManager,
); );
} }
@@ -118,7 +123,7 @@ class DatevEmailSubscriberTest extends TestCase
$nonExistentPath = '/tmp/non_existent_file_'.uniqid().'.pdf'; $nonExistentPath = '/tmp/non_existent_file_'.uniqid().'.pdf';
$this->uploadHandler $this->uploadHandler
->method('getUploadFilepath') ->method('getResolvedUploadFilepath')
->with($document) ->with($document)
->willReturn($nonExistentPath); ->willReturn($nonExistentPath);
@@ -160,7 +165,7 @@ class DatevEmailSubscriberTest extends TestCase
try { try {
$this->uploadHandler $this->uploadHandler
->expects($this->once()) ->expects($this->once())
->method('getUploadFilepath') ->method('getResolvedUploadFilepath')
->with($document) ->with($document)
->willReturn($tmpFile); ->willReturn($tmpFile);
+1
View File
@@ -0,0 +1 @@
upload.orig.pdf
+8 -1
View File
@@ -14,6 +14,8 @@ use App\Entity\Upload;
use App\Service\Pdf\InvoiceApprover; use App\Service\Pdf\InvoiceApprover;
use App\Service\Pdf\InvoiceRenderer; use App\Service\Pdf\InvoiceRenderer;
use App\Service\Pdf\Pdf; use App\Service\Pdf\Pdf;
use App\Service\Upload\UploadHandler;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Filesystem\Filesystem;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@@ -25,6 +27,7 @@ class InvoiceApproverTest extends WebTestCase
private string $pdfPath; private string $pdfPath;
private string $origPath; private string $origPath;
private Filesystem $filesystem; private Filesystem $filesystem;
private UploadHandler&MockObject $uploadHandler;
protected function setUp(): void protected function setUp(): void
{ {
@@ -35,6 +38,7 @@ class InvoiceApproverTest extends WebTestCase
$this->pdfPath = $this->invoiceDir.'/test_approver_backup.pdf'; $this->pdfPath = $this->invoiceDir.'/test_approver_backup.pdf';
$this->origPath = $this->invoiceDir.'/test_approver_backup.orig.pdf'; $this->origPath = $this->invoiceDir.'/test_approver_backup.orig.pdf';
$this->filesystem = new Filesystem(); $this->filesystem = new Filesystem();
$this->uploadHandler = $this->createMock(UploadHandler::class);
$this->filesystem->mkdir($this->invoiceDir); $this->filesystem->mkdir($this->invoiceDir);
} }
@@ -80,7 +84,7 @@ class InvoiceApproverTest extends WebTestCase
$translator = $this->createMock(TranslatorInterface::class); $translator = $this->createMock(TranslatorInterface::class);
/** @var TranslatorInterface $translator */ /** @var TranslatorInterface $translator */
return new InvoiceApprover($translator, ['project_dir' => $this->projectDir]); return new InvoiceApprover($translator, ['project_dir' => $this->projectDir], $this->uploadHandler);
} }
private function createDispositionWithInvoicePdf(): Disposition private function createDispositionWithInvoicePdf(): Disposition
@@ -130,6 +134,9 @@ class InvoiceApproverTest extends WebTestCase
; ;
$disposition->addDocument($invoice); $disposition->addDocument($invoice);
$this->uploadHandler->method('getResolvedUploadFilepath')->with($invoice)->willReturn($this->pdfPath);
$this->uploadHandler->method('getUploadFilepath')->with($invoice)->willReturn($this->pdfPath);
return $disposition; return $disposition;
} }
} }
+7 -1
View File
@@ -16,6 +16,7 @@ use App\Entity\User;
use App\Service\Pdf\InvoiceApprover; use App\Service\Pdf\InvoiceApprover;
use App\Service\Pdf\InvoiceRenderer; use App\Service\Pdf\InvoiceRenderer;
use App\Service\Pdf\Pdf; use App\Service\Pdf\Pdf;
use App\Service\Upload\UploadHandler;
use Carbon\CarbonPeriod; use Carbon\CarbonPeriod;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Contracts\Translation\TranslatorInterface;
@@ -108,8 +109,13 @@ class InvoiceRendererTest extends WebTestCase
; ;
$disposition->addDocument($invoice); $disposition->addDocument($invoice);
$invoicePath = $projectDir.'/uploads/invoice/a/abcdef0123456789.pdf';
$uploadHandler = $this->createMock(UploadHandler::class);
$uploadHandler->method('getResolvedUploadFilepath')->with($invoice)->willReturn($invoicePath);
$uploadHandler->method('getUploadFilepath')->with($invoice)->willReturn($invoicePath);
/** @var TranslatorInterface $translator */ /** @var TranslatorInterface $translator */
$approver = new InvoiceApprover($translator, ['project_dir' => $projectDir]); $approver = new InvoiceApprover($translator, ['project_dir' => $projectDir], $uploadHandler);
$pdf = $approver->render($disposition); $pdf = $approver->render($disposition);
$this->assertInstanceOf(Pdf::class, $pdf); $this->assertInstanceOf(Pdf::class, $pdf);
} }