createDisposition(createdAt: 'today -2 days', assignmentEndsIn: '+30 days'); $this->assertFalse($disposition->isContractDue(self::CONTRACT_DEADLINE_DAYS)); } public function testTheContractIsDueOnceTheUploadPeriodHasRunOut(): void { $disposition = $this->createDisposition(createdAt: 'today -6 days', assignmentEndsIn: '+30 days'); $this->assertTrue($disposition->isContractDue(self::CONTRACT_DEADLINE_DAYS)); } /** * The upload is blocked from the day before the assignment ends, so past that point the * dashboard would be asking for something the workflow guard refuses. */ public function testTheContractStopsBeingDueOnceTheUploadIsBlocked(): void { $disposition = $this->createDisposition(createdAt: 'today -60 days', assignmentEndsIn: '+1 day'); $this->assertFalse($disposition->isContractDue(self::CONTRACT_DEADLINE_DAYS)); } /** * A short-notice assignment can end before the normal $deadlineDays window would even * start (assignment ends in 4 days, but the normal deadline is 5 days out). Left uncapped, * dueDateFrom > dueDateTo made isStarted() and isEnded() equal at every instant, so this * never returned true. */ public function testTheContractIsDueImmediatelyWhenTheAssignmentEndsBeforeTheNormalDeadline(): void { $disposition = $this->createDisposition(createdAt: 'today', assignmentEndsIn: '+4 days'); $this->assertTrue($disposition->isContractDue(self::CONTRACT_DEADLINE_DAYS)); } public function testAnUploadedContractIsNeverDue(): void { $disposition = $this->createDisposition(createdAt: 'today -60 days', assignmentEndsIn: '+30 days'); $disposition->addDocument((new Upload())->setType(Upload::TYPE_CONTRACT)); $this->assertFalse($disposition->isContractDue(self::CONTRACT_DEADLINE_DAYS)); } public function testTheInvoiceIsNotDueWhileTheAssignmentIsStillRunning(): void { $disposition = $this->createDisposition(createdAt: 'today -30 days', assignmentEndsIn: '+5 days'); $this->assertFalse($disposition->isInvoiceDue(self::INVOICE_DEADLINE_DAYS)); } public function testTheInvoiceIsDueOnceTheAssignmentHasEnded(): void { $disposition = $this->createDisposition(createdAt: 'today -30 days', assignmentEndsIn: '-1 day'); $this->assertTrue($disposition->isInvoiceDue(self::INVOICE_DEADLINE_DAYS)); } /** * Nothing blocks a late invoice upload, so unlike the contract this must keep asking. */ public function testTheInvoiceStaysDueAfterTheUploadPeriodHasRunOut(): void { $disposition = $this->createDisposition(createdAt: 'today -90 days', assignmentEndsIn: '-60 days'); $this->assertTrue($disposition->isInvoiceDue(self::INVOICE_DEADLINE_DAYS)); } /** * A skip-formalities assignment has no contract step at all, so a placement on one starts * where a signed and checked contract would otherwise have put it. */ public function testAPlacementOnASkipFormalitiesAssignmentIsConfirmedRightAway(): void { $disposition = $this->createDisposition( createdAt: 'today', assignmentEndsIn: '+30 days', skipFormalities: true, ); $this->assertSame(Disposition::STATUS_CONFIRMED, $disposition->getStatus()); $this->assertTrue($disposition->isSkipFormalities()); } public function testAPlacementOnANormalAssignmentStillStartsAsNew(): void { $disposition = $this->createDisposition(createdAt: 'today', assignmentEndsIn: '+30 days'); $this->assertSame(Disposition::STATUS_NEW, $disposition->getStatus()); $this->assertFalse($disposition->isSkipFormalities()); } /** * Both dates are chosen so the unflagged disposition would be due - what is pinned here is * that the flag wins over the period maths, not that the period happens to be closed. */ public function testNoDocumentIsEverDueOnASkipFormalitiesAssignment(): void { $disposition = $this->createDisposition( createdAt: 'today -60 days', assignmentEndsIn: '-1 day', skipFormalities: true, ); $this->assertFalse($disposition->isContractDue(self::CONTRACT_DEADLINE_DAYS)); $this->assertFalse($disposition->isInvoiceDue(self::INVOICE_DEADLINE_DAYS)); } public function testARejectedContractIsNotReportedOnASkipFormalitiesAssignment(): void { $disposition = $this->createDisposition( createdAt: 'today -60 days', assignmentEndsIn: '+30 days', skipFormalities: true, ); $disposition->addDocument( (new Upload())->setType(Upload::TYPE_CONTRACT)->setStatus(Upload::STATUS_REJECTED) ); $this->assertFalse($disposition->isContractRejected()); } /** * getDocumentByType() answers with the first match and is right for the Honorarvertrag and the * Honorarnote, of which there is one each. Belege are the first type a disposition can hold * several of, so they need the collection-returning sibling. */ public function testTheReceiptsOfADispositionAreReturnedAsACollection(): void { $disposition = $this->createDisposition(createdAt: 'today', assignmentEndsIn: '-1 day'); $disposition ->addDocument((new Upload())->setType(Upload::TYPE_INVOICE)) ->addDocument((new Upload())->setType(Upload::TYPE_RECEIPT)->setOriginalFilename('bahn.pdf')) ->addDocument((new Upload())->setType(Upload::TYPE_CONTRACT)) ->addDocument((new Upload())->setType(Upload::TYPE_RECEIPT)->setOriginalFilename('taxi.jpg')) ; $receipts = $disposition->getDocumentsByType(Upload::TYPE_RECEIPT); $this->assertCount(2, $receipts); $this->assertSame( ['bahn.pdf', 'taxi.jpg'], array_values(array_map( fn (Upload $upload): string => $upload->getOriginalFilename(), $receipts->toArray() )) ); // The single-result sibling keeps working for the types that only ever have one. $this->assertSame(Upload::TYPE_INVOICE, $disposition->getDocumentByType(Upload::TYPE_INVOICE)?->getType()); } public function testADispositionWithoutReceiptsReturnsAnEmptyCollection(): void { $disposition = $this->createDisposition(createdAt: 'today', assignmentEndsIn: '-1 day'); $disposition->addDocument((new Upload())->setType(Upload::TYPE_INVOICE)); $this->assertCount(0, $disposition->getDocumentsByType(Upload::TYPE_RECEIPT)); } private function createDisposition( string $createdAt, string $assignmentEndsIn, bool $skipFormalities = false, ): Disposition { $destination = (new Destination()) ->setProduct('Skireise') ->setDateFrom(new \DateTimeImmutable($assignmentEndsIn.' -7 days')) ->setDateTo(new \DateTimeImmutable($assignmentEndsIn)) ; $assignment = (new Assignment()) ->setDestination($destination) ->setSkipFormalities($skipFormalities) ; $disposition = new Disposition(new Application($assignment, new Teamer())); return $disposition->setCreatedAt(new \DateTimeImmutable($createdAt)); } }