21 Commits
Author SHA1 Message Date
Torstem_JetBrain d11070ffbb Ergebnis bei *PHPStan* verbessert. 2021-04-06 11:15:25 +02:00
Torstem_JetBrain 5cdc11d9df Coverage verbessert. 2021-04-06 10:57:17 +02:00
Torstem_JetBrain add7895191 Kleine Verbesserungen. 2021-04-06 10:44:46 +02:00
Torstem_JetBrain 20fec2dc07 Korrektur der Schreibweise. 2021-04-06 10:41:40 +02:00
Torstem_JetBrain 123f5eef81 Es wird bei den Wochen der erwartete Tag angezeigt. 2021-04-02 21:58:06 +02:00
Torstem_JetBrain 0e98a933e1 Zur besseren Kontrolle wird auch das erwartete Perioden-Datum überprüft. 2021-04-02 21:36:37 +02:00
Torstem_JetBrain c2676abfa2 Die einzelnen Perioden-Klassen werden erfolgreich getestet. 2021-04-02 21:19:15 +02:00
Torstem_JetBrain 675c5cb9ae Die Unterschiede zwischen den einzelnen Perioden-Repository ist größer als gedacht. Deshalb hat jede Periode seine eigene Klasse bekommen. 2021-04-02 21:16:57 +02:00
Torstem_JetBrain c8219d9c88 Tests für die Repository der Table-Views erstellt. 2021-04-02 19:21:41 +02:00
Torstem_JetBrain edc7ed5fe6 Definition hatte keinen primären Schlüssel. 2021-04-02 19:20:53 +02:00
Torstem_JetBrain e113a80efc Es wird getestet, das die Nutzung falscher Modelle das erforderliche Verhalten hat. 2021-04-01 14:11:02 +02:00
Torstem_JetBrain 397416cd31 Format des Code verbessert. 2021-04-01 14:03:35 +02:00
Torstem_JetBrain d0799eaae2 Die Tests für das Schreiben der Daten sind erfolgreich. 2021-04-01 14:00:41 +02:00
Torstem_JetBrain 7e8ed66cb7 Die Tests für das Lesen der Daten sind erfolgreich. 2021-04-01 12:16:03 +02:00
Torstem_JetBrain 93f52af52b Tests für den Record-Klasse geschrieben. 2021-03-31 15:40:21 +02:00
Torstem_JetBrain 610e892063 Fehler beseitigt, die beim Schreiben der Tests auffielen. 2021-03-31 15:39:35 +02:00
Torstem_JetBrain 7e8d4fbdf4 Vorbereitung der Datenbank-Test-Verbindung 2021-03-31 14:24:46 +02:00
Torstem_JetBrain 5141fff827 Interface und Fehlerklassen für die Repositories bestimmt. 2021-03-30 13:31:18 +02:00
Torstem_JetBrain e6b9180432 Die Modelle für die Ansichten wurden definiert, einschließlich der Tests. 2021-03-29 16:31:04 +02:00
Torstem_JetBrain b7b72d9044 De neuen Tests garantieren, den Zusammenhang zwischen dem 'Setter' und 'Getter'. 2021-03-29 16:20:33 +02:00
Torstem_JetBrain e4cd00caf1 Einführung des Modells. 2021-03-29 15:07:07 +02:00
24 changed files with 1371 additions and 6 deletions
-2
View File
@@ -104,7 +104,5 @@ Temporary Items
/api/codeception.yml /api/codeception.yml
/api/vendor/ /api/vendor/
/api/tests/_* /api/tests/_*
/api/tests/acceptance
/api/tests/functional
/api/tests/*.suite.yml /api/tests/*.suite.yml
/api/html/swagger/ /api/html/swagger/
+23
View File
@@ -0,0 +1,23 @@
paths:
tests: tests
output: tests/_output
data: tests/_data
support: tests/_support
envs: tests/_envs
actor_suffix: Tester
extensions:
enabled:
- Codeception\Extension\RunFailed
modules:
enabled:
- Db:
dsn: 'pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb'
user: 'bruce'
password: 'mypass'
dump: tests/_data/dump.sql
cleanup: true # reload dump between tests
populate: true # load dump before all tests
coverage:
enabled: true
include:
- src/*
+5 -2
View File
@@ -18,13 +18,16 @@
"require": { "require": {
"slim/slim": "^4.7.1", "slim/slim": "^4.7.1",
"vlucas/phpdotenv": "^4.2", "vlucas/phpdotenv": "^4.2",
"slim/psr7": "^1.3" "slim/psr7": "^1.3",
"jetbrains/phpstorm-attributes": "^1.0.0",
"myclabs/php-enum": "^1.8.0"
}, },
"require-dev": { "require-dev": {
"phpstan/phpstan": "^0.12.80", "phpstan/phpstan": "^0.12.80",
"codeception/codeception": "^4.1.18", "codeception/codeception": "^4.1.18",
"codeception/module-phpbrowser": "^1.0.0", "codeception/module-phpbrowser": "^1.0.0",
"codeception/module-asserts": "^1.0.0" "codeception/module-asserts": "^1.0.0",
"codeception/module-db": "^1.1.0"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
+5
View File
@@ -0,0 +1,5 @@
parameters:
level: 6
paths:
- src
- tests/unit
+10 -2
View File
@@ -8,8 +8,16 @@ use Slim\Psr7\Request;
class HelloWorldController class HelloWorldController
{ {
/** @noinspection PhpUnusedParameterInspection */ /**
public function read(Request $request, Response $response, $args): Response * @param Request $request
* @param Response $response
* @param mixed[] $args
*
* @return Response
*
* @noinspection PhpUnusedParameterInspection
*/
public function read(Request $request, Response $response, array $args): Response
{ {
$payload = json_encode('Hallo World'); $payload = json_encode('Hallo World');
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Models;
use JsonSerializable;
interface ModelInterface extends JsonSerializable
{
}
+23
View File
@@ -0,0 +1,23 @@
<?php /** @noinspection PhpUnusedPrivateFieldInspection */
namespace TorstenHettstedt\TimekeepingApi\Models;
use MyCLabs\Enum\Enum;
/**
* Enum-Klasse für die unterstützten Periode-Definitionen.
* Der Wert der Periode ist ein Format-String für {@link https://secure.php.net/manual/en/datetime.format.php}.
*
* @extends Enum<string>
*
* @method static PeriodDesignationEnum WEEKLY()
* @method static PeriodDesignationEnum MONTHLY()
* @method static PeriodDesignationEnum YEARLY()
*/
class PeriodDesignationEnum extends Enum
{
private const WEEKLY = 'Y#W';
private const MONTHLY = 'Y-m';
private const YEARLY = 'Y';
}
+65
View File
@@ -0,0 +1,65 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Models;
use DateInterval;
use DateTime;
use JetBrains\PhpStorm\ArrayShape;
class WorkingHours implements ModelInterface
{
protected ?DateTime $workingDay = null;
protected ?DateInterval $workingTime = null;
/**
* @return DateTime|null
*/
public function getWorkingDay(): ?DateTime
{
return $this->workingDay;
}
/**
* @param DateTime $workingDay
*
* @return WorkingHours
*/
public function setWorkingDay(DateTime $workingDay): WorkingHours
{
$this->workingDay = $workingDay;
return $this;
}
/**
* @return DateInterval|null
*/
public function getWorkingTime(): ?DateInterval
{
return $this->workingTime;
}
/**
* @param DateInterval $workingTime
*
* @return WorkingHours
*/
public function setWorkingTime(DateInterval $workingTime): WorkingHours
{
$this->workingTime = $workingTime;
return $this;
}
/**
* @return array<string, string>
*/
#[ArrayShape(['workingDay' => "string", 'workingTime' => "string"])]
public function jsonSerialize(): array
{
return [
'workingDay' => $this->getWorkingDay()->format('Y-m-d'),
'workingTime' => $this->getWorkingTime()->format('%H:%I:%S')
];
}
}
+102
View File
@@ -0,0 +1,102 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Models;
use DateInterval;
use DateTimeInterface;
use JetBrains\PhpStorm\ArrayShape;
class WorkingHoursView implements ModelInterface
{
protected DateTimeInterface $period;
protected PeriodDesignationEnum $periodDesignation;
protected int $workingDays;
protected DateInterval $totalHours;
protected DateInterval $overtime;
/**
* WorkingHoursView constructor.
*
* @param DateTimeInterface $period
* @param PeriodDesignationEnum $periodDesignation
* @param int $workingDays
* @param DateInterval $totalHours
* @param DateInterval $overtime
*/
public function __construct(
DateTimeInterface $period,
PeriodDesignationEnum $periodDesignation,
int $workingDays,
DateInterval $totalHours,
DateInterval $overtime
) {
$this->period = $period;
$this->periodDesignation = $periodDesignation;
$this->workingDays = $workingDays;
$this->totalHours = $totalHours;
$this->overtime = $overtime;
}
/**
* @return DateTimeInterface
*/
public function getPeriod(): DateTimeInterface
{
return $this->period;
}
/**
* @return PeriodDesignationEnum
*/
public function getPeriodDesignation(): PeriodDesignationEnum
{
return $this->periodDesignation;
}
/**
* @return int
*/
public function getWorkingDays(): int
{
return $this->workingDays;
}
/**
* @return DateInterval
*/
public function getTotalHours(): DateInterval
{
return $this->totalHours;
}
/**
* @return DateInterval
*/
public function getOvertime(): DateInterval
{
return $this->overtime;
}
/**
* @inheritDoc
*
* @return array<string, mixed>
*/
#[ArrayShape(['period' => "string",
'periodDesignation' => "string",
'totalHours' => "string",
'workingDays' => "int",
'overtime' => "string"
])] public function jsonSerialize(): array
{
return [
'period' => $this->getPeriod()->format($this->getPeriodDesignation()->getValue()),
'periodDesignation' => strtolower($this->getPeriodDesignation()->getKey()),
'workingDays' => $this->getWorkingDays(),
'totalHours' => $this->getTotalHours()->format('%H:%I:%S'),
'overtime' => $this->getOvertime()->format('%H:%I:%S')
];
}
}
@@ -0,0 +1,117 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
use DateInterval;
use DateTimeInterface;
use Exception;
use PDO;
use PDOException;
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
/**
* @implements RepositoryReaderInterface<WorkingHoursView>
*/
abstract class AbstractWorkingHoursViewRepository implements RepositoryReaderInterface
{
protected const SQL_SELECT = '';
protected const SQL_WHERE = '';
protected PDO $database;
protected PeriodDesignationEnum $periodDesignation;
/**
* WorkingHoursRepository constructor.
*
* @param PDO $database
*/
public function __construct(PDO $database)
{
$this->database = $database;
$this->database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
/**
*
* Gibt alle vorhandenen Einträge zurück
*
* @return WorkingHoursView[]
* @throws Exception
* @throws PDOException
*/
public function findAll(): array
{
$models = [];
$stmt = $this->database->prepare(static::SQL_SELECT);
$stmt->execute();
while ($row = $stmt->fetch()) {
$model = new WorkingHoursView(
$this->buildDateFromPeriod($row['period']),
$this->periodDesignation,
(int) $row['workingDays'],
$this->buildDateInterval($row['totalHours']),
$this->buildDateInterval($row['overtime'])
);
$models[] = $model;
}
return $models;
}
/**
* Gibt den Eintrag zurück, der durch die ID
*
* @param mixed $primary_key
*
* @return WorkingHoursView
* @throws RepositoryRecordNotFoundException
* @throws Exception
* @throws PDOException
*/
public function findByKey(mixed $primary_key): WorkingHoursView
{
$stmt = $this->database->prepare(static::SQL_SELECT . static::SQL_WHERE);
$stmt->bindParam(1, $primary_key);
$stmt->execute();
$row = $stmt->fetch();
if ($row === false) {
throw new RepositoryRecordNotFoundException();
}
return new WorkingHoursView(
$this->buildDateFromPeriod($row['period']),
$this->periodDesignation,
(int) $row['workingDays'],
$this->buildDateInterval($row['totalHours']),
$this->buildDateInterval($row['overtime'])
);
}
/**
* @param string $time
*
* @return DateInterval
* @throws Exception
*/
protected function buildDateInterval(string $time): DateInterval
{
$time_array = explode(':', $time);
$hours = (int)$time_array[0];
$minutes = (int)$time_array[1];
$seconds = (int)$time_array[2];
$interval = new DateInterval(sprintf('PT%dH%dM%dS', abs($hours), abs($minutes), abs($seconds)));
if (($hours < 0 || ($minutes < 0) || ($seconds < 0))) {
$interval->invert = 1;
}
return $interval;
}
/**
* @param string $period
*
* @return DateTimeInterface
* @throws Exception
*/
abstract protected function buildDateFromPeriod(string $period): DateTimeInterface;
}
@@ -0,0 +1,12 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
use Exception;
class RepositoryException extends Exception
{
}
@@ -0,0 +1,10 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
class RepositoryModelAlreadyExists extends RepositoryException
{
}
@@ -0,0 +1,28 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
/**
* @template T of \TorstenHettstedt\TimekeepingApi\Models\ModelInterface
*/
interface RepositoryReaderInterface
{
/**
*
* Gibt alle vorhandenen Einträge zurück
*
* @return T[]
*/
public function findAll(): array;
/**
* Gibt den Eintrag zurück, der durch die ID
*
* @param mixed $primary_key
*
* @return T
* @throws RepositoryRecordNotFoundException
*/
public function findByKey(mixed $primary_key);
}
@@ -0,0 +1,10 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
class RepositoryRecordNotFoundException extends RepositoryException
{
}
@@ -0,0 +1,31 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
/**
* @template T of \TorstenHettstedt\TimekeepingApi\Models\ModelInterface
*/
interface RepositoryWriterInterface
{
/**
* @param T $model
*
* @throws RepositoryModelAlreadyExists
*/
public function insert(mixed $model): void;
/**
* @param T $model
*
* @throws RepositoryRecordNotFoundException
*/
public function update(mixed $model): void;
/**
* @param T $model
*
* @throws RepositoryRecordNotFoundException
*/
public function delete(mixed $model): void;
}
@@ -0,0 +1,38 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
use DateTime;
use DateTimeInterface;
use PDO;
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
class WorkingHoursMonthlyViewRepository extends AbstractWorkingHoursViewRepository
{
protected const SQL_SELECT = <<<SQL
select
"Monat" as "period",
"Gesamtarbeitszeit" as "totalHours",
"Arbeitstage" as "workingDays",
"Überstunden" as "overtime"
from "Arbeitszeiten - Monat"
SQL;
protected const SQL_WHERE = ' where "Monat" = ?';
protected PDO $database;
protected PeriodDesignationEnum $periodDesignation;
public function __construct(PDO $database)
{
parent::__construct($database);
$this->periodDesignation = PeriodDesignationEnum::MONTHLY();
}
protected function buildDateFromPeriod(string $period): DateTimeInterface
{
return new DateTime($period . '-01');
}
}
@@ -0,0 +1,153 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
use DateInterval;
use DateTime;
use Exception;
use InvalidArgumentException;
use PDO;
use PDOException;
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
/**
* @implements RepositoryReaderInterface<WorkingHours>
* @implements RepositoryWriterInterface<WorkingHours>
*/
class WorkingHoursRepository implements RepositoryReaderInterface, RepositoryWriterInterface
{
protected PDO $database;
/**
* WorkingHoursRepository constructor.
*
* @param PDO $database
*/
public function __construct(PDO $database)
{
$this->database = $database;
$this->database->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
}
/**
*
* Gibt alle vorhandenen Einträge zurück
*
* @return WorkingHours[]
* @throws Exception
* @throws PDOException
*/
public function findAll(): array
{
$models = [];
$stmt = $this->database->prepare('select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten"');
$stmt->execute();
while ($row = $stmt->fetch()) {
$model = new WorkingHours();
$model
->setWorkingDay(new DateTime($row['workingDay']))
->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime']));
$models[] = $model;
}
return $models;
}
/**
* Gibt den Eintrag zurück, der durch die ID
*
* @param mixed $primary_key
*
* @return WorkingHours
* @throws RepositoryRecordNotFoundException
* @throws Exception
* @throws PDOException
*/
public function findByKey(mixed $primary_key): WorkingHours
{
$stmt = $this->database->prepare('select "Datum" as "workingDay", "Arbeitszeit" as "workingTime" from public."Arbeitszeiten" where "Datum" = ?');
$stmt->bindParam(1, $primary_key);
$stmt->execute();
$row = $stmt->fetch();
if ($row === false) {
throw new RepositoryRecordNotFoundException();
}
$model = new WorkingHours();
$model
->setWorkingDay(new DateTime($row['workingDay']))
->setWorkingTime(new DateInterval('P0000-00-00T' . $row['workingTime']));
return $model;
}
/**
* @param WorkingHours|ModelInterface $model
*
* @throws RepositoryModelAlreadyExists
* @throws Exception
*/
public function insert(mixed $model): void
{
if ($model instanceof WorkingHours) {
$workingDay = $model->getWorkingDay()->format('Y-m-d');
$workingTime = $model->getWorkingTime()->format('%H:%I:%S');
} else {
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.');
}
try {
$this->findByKey($workingDay);
throw new RepositoryModelAlreadyExists();
} /** @noinspection PhpUnusedLocalVariableInspection */
catch (RepositoryRecordNotFoundException $exception) {
$stmt = $this->database->prepare('insert into public."Arbeitszeiten" ("Datum", "Arbeitszeit") values (?, ?) on conflict do nothing');
$stmt->bindParam(1, $workingDay);
$stmt->bindParam(2, $workingTime);
$stmt->execute();
}
}
/**
* @param WorkingHours|ModelInterface $model
*
* @throws RepositoryRecordNotFoundException
*/
public function update(mixed $model): void
{
if ($model instanceof WorkingHours) {
$workingDay = $model->getWorkingDay()->format('Y-m-d');
$workingTime = $model->getWorkingTime()->format('%H:%I:%S');
} else {
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.');
}
$stmt = $this->database->prepare('update public."Arbeitszeiten" set "Arbeitszeit" = ? where "Datum" = ?');
$stmt->bindParam(1, $workingTime);
$stmt->bindParam(2, $workingDay);
$stmt->execute();
if ($stmt->rowCount() < 1) {
throw new RepositoryRecordNotFoundException();
}
}
/**
* @param WorkingHours|ModelInterface $model
*
* @throws RepositoryRecordNotFoundException
*/
public function delete(mixed $model): void
{
if ($model instanceof WorkingHours) {
$workingDay = $model->getWorkingDay()->format('Y-m-d');
} else {
throw new InvalidArgumentException('Es wird ein Modell der Klasse "' . WorkingHours::class . '" verlangt.');
}
$stmt = $this->database->prepare('delete from public."Arbeitszeiten" where "Datum" = ?');
$stmt->bindParam(1, $workingDay);
$stmt->execute();
if ($stmt->rowCount() < 1) {
throw new RepositoryRecordNotFoundException();
}
}
}
@@ -0,0 +1,43 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
use DateInterval;
use DateTime;
use DateTimeInterface;
use PDO;
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
class WorkingHoursWeeklyViewRepository extends AbstractWorkingHoursViewRepository
{
protected const SQL_SELECT = <<<SQL
select
"Woche" as "period",
"Gesamtarbeitszeit" as "totalHours",
"Arbeitstage" as "workingDays",
"Überstunden" as "overtime"
from "Arbeitszeiten - Woche"
SQL;
protected const SQL_WHERE = ' where "Woche" = ?';
protected PDO $database;
protected PeriodDesignationEnum $periodDesignation;
public function __construct(PDO $database)
{
parent::__construct($database);
$this->periodDesignation = PeriodDesignationEnum::WEEKLY();
}
protected function buildDateFromPeriod(string $period): DateTimeInterface
{
$time_array = explode('#', $period);
$year = (int)$time_array[0];
$week = (int)$time_array[1] - 1;
$date = new DateTime('first day of January ' . $year);
$date->modify('monday this week');
$date->add(new DateInterval(sprintf('P%dW', $week)));
return $date;
}
}
@@ -0,0 +1,43 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Repositories;
use DateTime;
use DateTimeInterface;
use Exception;
use PDO;
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
class WorkingHoursYearlyViewRepository extends AbstractWorkingHoursViewRepository
{
protected const SQL_SELECT = <<<SQL
select
"Jahr" as "period",
"Gesamtarbeitszeit" as "totalHours",
"Arbeitstage" as "workingDays",
"Überstunden" as "overtime"
from "Arbeitszeiten - Jahr"
SQL;
protected const SQL_WHERE = ' where "Jahr" = ?';
protected PDO $database;
protected PeriodDesignationEnum $periodDesignation;
public function __construct(PDO $database)
{
parent::__construct($database);
$this->periodDesignation = PeriodDesignationEnum::YEARLY();
}
/**
* @param string $period
*
* @return DateTimeInterface
* @throws Exception
*/
protected function buildDateFromPeriod(string $period): DateTimeInterface
{
return new DateTime($period . '-01-01');
}
}
+148
View File
@@ -0,0 +1,148 @@
--
-- PostgreSQL database dump
--
SET statement_timeout = 0;
SET lock_timeout = 0;
SET idle_in_transaction_session_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SELECT pg_catalog.set_config('search_path', '', false);
SET check_function_bodies = false;
SET xmloption = content;
SET client_min_messages = warning;
SET row_security = off;
DROP TABLE IF EXISTS public."Arbeitszeiten";
DROP VIEW IF EXISTS public."Arbeitszeiten - Woche";
DROP VIEW IF EXISTS public."Arbeitszeiten - Monat";
DROP VIEW IF EXISTS public."Arbeitszeiten - Jahr";
--
-- Name: Arbeitszeiten; Type: TABLE; Schema: public; Owner: torsten
--
CREATE TABLE public."Arbeitszeiten" (
"Datum" date not null,
"Arbeitszeit" interval(6) null,
constraint "Arbeitszeiten_pkey" primary key ("Datum")
);
ALTER TABLE public."Arbeitszeiten" OWNER TO bruce;
--
-- Name: Arbeitszeiten - Jahr; Type: VIEW; Schema: public; Owner: torsten
--
CREATE OR REPLACE VIEW public."Arbeitszeiten - Jahr" AS
SELECT (date_part('year'::text, "Arbeitszeiten"."Datum"))::text AS "Jahr",
sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit",
count("Arbeitszeiten"."Datum") AS "Arbeitstage",
(sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden"
FROM public."Arbeitszeiten"
GROUP BY (date_part('year'::text, "Arbeitszeiten"."Datum"))
ORDER BY (date_part('year'::text, "Arbeitszeiten"."Datum"));
ALTER TABLE public."Arbeitszeiten - Jahr" OWNER TO bruce;
--
-- Name: Arbeitszeiten - Monat; Type: VIEW; Schema: public; Owner: torsten
--
CREATE OR REPLACE VIEW public."Arbeitszeiten - Monat" AS
SELECT to_char(date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone), 'TMYYYY TMMonth'::text) AS "Monat",
sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit",
count("Arbeitszeiten"."Datum") AS "Arbeitstage",
(sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden"
FROM public."Arbeitszeiten"
GROUP BY (date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone))
ORDER BY (date_trunc('month'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone));
ALTER TABLE public."Arbeitszeiten - Monat" OWNER TO bruce;
--
-- Name: Arbeitszeiten - Woche; Type: VIEW; Schema: public; Owner: torsten
--
CREATE OR REPLACE VIEW public."Arbeitszeiten - Woche" AS
SELECT to_char(date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone), 'TMYYYY#WW'::text) AS "Woche",
sum("Arbeitszeiten"."Arbeitszeit") AS "Gesamtarbeitszeit",
count("Arbeitszeiten"."Datum") AS "Arbeitstage",
(sum("Arbeitszeiten"."Arbeitszeit") - ((count("Arbeitszeiten"."Datum"))::double precision * '08:00:00'::interval)) AS "Überstunden"
FROM public."Arbeitszeiten"
GROUP BY (date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone))
ORDER BY (date_trunc('week'::text, ("Arbeitszeiten"."Datum")::timestamp with time zone));
ALTER TABLE public."Arbeitszeiten - Woche" OWNER TO bruce;
--
-- Data for Name: Arbeitszeiten; Type: TABLE DATA; Schema: public; Owner: torsten
--
COPY public."Arbeitszeiten" ("Datum", "Arbeitszeit") FROM stdin;
2020-01-07 08:06:00
2020-01-08 08:16:00
2020-01-09 07:49:00
2020-01-10 07:30:00
2020-01-13 08:06:00
2020-01-14 08:04:00
2020-01-15 08:20:00
2020-01-16 07:13:00
2020-01-17 07:57:00
2020-01-20 07:53:00
2020-01-21 07:49:00
2020-01-22 07:56:00
2020-01-23 07:55:00
2020-01-24 07:08:00
2020-01-27 08:23:00
2020-01-28 07:20:00
2020-01-29 08:13:00
2020-01-30 08:43:00
2020-01-31 07:21:00
2020-02-03 07:56:00
2020-02-04 08:05:00
2020-02-05 07:58:00
2020-02-06 08:01:00
2020-02-07 07:59:00
2020-02-10 07:50:00
2020-02-11 08:01:00
2020-02-12 08:14:00
2020-02-13 08:12:00
2020-02-14 07:51:00
2020-02-17 07:59:00
2020-02-18 08:05:00
2020-02-19 07:34:00
2020-02-20 07:33:00
2020-02-21 07:56:00
2020-02-24 08:02:00
2020-02-25 08:13:00
2020-02-26 08:42:00
2020-02-27 07:49:00
2020-02-28 08:16:00
2020-03-02 08:22:00
2020-03-03 08:20:00
2020-03-04 08:12:00
2020-03-05 08:32:00
2020-03-06 04:41:00
2020-03-09 07:05:00
2020-03-10 07:34:00
2020-03-11 07:39:00
2020-03-12 07:50:00
2020-03-13 08:26:00
2020-03-16 07:51:00
2020-03-17 07:50:00
2020-03-18 07:19:00
2020-03-19 07:55:00
2020-03-20 05:43:00
2020-03-23 08:05:00
2020-03-24 08:21:00
2020-03-25 08:10:00
2020-03-26 10:31:00
2020-03-27 04:55:00
2020-03-30 08:21:00
2020-03-31 08:01:00
\.
@@ -0,0 +1,76 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Models;
use Codeception\Test\Unit;
use DateInterval;
use DateTime;
use Error;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
class WorkingHoursTest extends Unit
{
public function testWorkingHoursWithoutContent(): void
{
$obj = new WorkingHours();
$this->assertNull($obj->getWorkingDay());
$this->assertNull($obj->getWorkingTime());
$this->expectException(Error::class);
$obj->jsonSerialize();
}
/**
* @return array[]
*/
public function workingHoursProvider(): array
{
return [
[
new DateTime('2020-11-30'),
new DateInterval("PT8H"),
[
'workingDay' => '2020-11-30',
'workingTime' => '08:00:00',
],
],
[
new DateTime('2021-05-03'),
new DateInterval("PT7H45M"),
[
'workingDay' => '2021-05-03',
'workingTime' => '07:45:00',
],
],
];
}
/**
* @param DateTime $date
* @param DateInterval $interval
* @param array<string, string> $should_json
*
* @dataProvider workingHoursProvider
*/
public function testWorkingHoursWithContent(DateTime $date, DateInterval $interval, array $should_json): void
{
$obj = new WorkingHours();
$obj
->setWorkingDay($date)
->setWorkingTime($interval);
$this->assertNotNull($obj->getWorkingDay());
$this->assertInstanceOf(DateTime::class, $obj->getWorkingDay());
$this->assertEquals($date, $obj->getWorkingDay());
$this->assertNotNull($obj->getWorkingTime());
$this->assertInstanceOf(DateInterval::class, $obj->getWorkingTime());
$this->assertEquals($interval, $obj->getWorkingTime());
$json = $obj->jsonSerialize();
$this->assertIsArray($json);
$this->assertEquals($should_json, $json);
}
}
@@ -0,0 +1,113 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Models;
use Codeception\Test\Unit;
use DateInterval;
use DateTime;
use DateTimeInterface;
use TorstenHettstedt\TimekeepingApi\Models\PeriodDesignationEnum;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
class WorkingHoursViewTest extends Unit
{
/**
* @return array[]
*/
public function workingHoursProvider(): array
{
return [
[
new DateTime('2020-11-30'),
PeriodDesignationEnum::WEEKLY(),
15,
new DateInterval("PT32H22M"),
new DateInterval("PT22M"),
[
'period' => '2020#49',
'periodDesignation' => 'weekly',
'workingDays' => 15,
'totalHours' => '32:22:00',
'overtime' => '00:22:00'
],
],
[
new DateTime('2020-11-30'),
PeriodDesignationEnum::MONTHLY(),
15,
new DateInterval("PT32H22M"),
new DateInterval("PT22M"),
[
'period' => '2020-11',
'periodDesignation' => 'monthly',
'workingDays' => 15,
'totalHours' => '32:22:00',
'overtime' => '00:22:00'
],
],
[
new DateTime('2020-11-30'),
PeriodDesignationEnum::YEARLY(),
15,
new DateInterval("PT32H22M"),
new DateInterval("PT22M"),
[
'period' => '2020',
'periodDesignation' => 'yearly',
'workingDays' => 15,
'totalHours' => '32:22:00',
'overtime' => '00:22:00'
],
],
];
}
/**
* @param DateTimeInterface $period
* @param PeriodDesignationEnum $periodDesignation
* @param int $workingDays
* @param DateInterval $totalHours
* @param DateInterval $overtime
* @param array<string, string> $should_json
*
* @dataProvider workingHoursProvider
*/
public function testWorkingHoursViewWithContent(
DateTimeInterface $period,
PeriodDesignationEnum $periodDesignation,
int $workingDays,
DateInterval $totalHours,
DateInterval $overtime,
array $should_json
): void
{
$obj = new WorkingHoursView($period, $periodDesignation, $workingDays, $totalHours, $overtime);
$this->assertNotNull($obj->getPeriod());
$this->assertInstanceOf(DateTimeInterface::class, $obj->getPeriod());
$this->assertEquals($period, $obj->getPeriod());
$this->assertNotNull($obj->getPeriodDesignation());
$this->assertInstanceOf(PeriodDesignationEnum::class, $obj->getPeriodDesignation());
$this->assertEquals($periodDesignation, $obj->getPeriodDesignation());
$this->assertNotNull($obj->getWorkingDays());
$this->assertIsInt($obj->getWorkingDays());
$this->assertEquals($workingDays, $obj->getWorkingDays());
$this->assertNotNull($obj->getTotalHours());
$this->assertInstanceOf(DateInterval::class, $obj->getTotalHours());
$this->assertEquals($totalHours, $obj->getTotalHours());
$this->assertNotNull($obj->getOvertime());
$this->assertInstanceOf(DateInterval::class, $obj->getOvertime());
$this->assertEquals($overtime, $obj->getOvertime());
$json = $obj->jsonSerialize();
$this->assertIsArray($json);
$this->assertEquals($should_json, $json);
}
}
@@ -0,0 +1,195 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories;
use Codeception\Test\Unit;
use DateInterval;
use DateTime;
use Exception;
use InvalidArgumentException;
use PDO;
use PDOStatement;
use TorstenHettstedt\TimekeepingApi\Models\ModelInterface;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHours;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryModelAlreadyExists;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursRepository;
class WorkingHoursRepositoryTest extends Unit
{
const EXISTING_DATE = '2020-01-28';
const EXISTING_INTERVAL = 'PT7H20M';
const NEW_DATE = '2020-04-01';
const NEW_INTERVAL = 'PT7H59M';
const RECORDS_COUNT = 61;
protected PDO $pdoObject;
protected PDOStatement $pdoStatement;
protected function _before(): void
{
/** @noinspection SpellCheckingInspection */
$this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass');
parent::_before();
}
/**
* @throws Exception
*/
public function testFindAll(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$models = $repository->findAll();
$this->assertIsArray($models);
$this->assertContainsOnly(WorkingHours::class, $models);
$this->assertCount(self::RECORDS_COUNT, $models);
}
/**
* @throws RepositoryRecordNotFoundException
*/
public function testExistingFindByKey(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $repository->findByKey(self::EXISTING_DATE);
$this->assertInstanceOf(WorkingHours::class, $model);
$this->assertEquals(new DateTime(self::EXISTING_DATE), $model->getWorkingDay());
$this->assertEquals(new DateInterval(self::EXISTING_INTERVAL), $model->getWorkingTime());
}
public function testNotExistingFindByKey(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$this->expectException(RepositoryRecordNotFoundException::class);
$repository->findByKey(self::NEW_DATE);
}
/**
* @throws Exception
*/
public function testInsertWithInvalidModel(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->makeEmpty(ModelInterface::class);
$this->expectException(InvalidArgumentException::class);
$repository->insert($model);
}
/**
* @throws RepositoryRecordNotFoundException
* @throws Exception
*/
public function testNewInsert(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::NEW_DATE),
'workingTime' => new DateInterval(self::NEW_INTERVAL),
]);
$repository->insert($model);
$model = $repository->findByKey(self::NEW_DATE);
$this->assertInstanceOf(WorkingHours::class, $model);
$this->assertEquals(new DateTime(self::NEW_DATE), $model->getWorkingDay());
$this->assertEquals(new DateInterval(self::NEW_INTERVAL), $model->getWorkingTime());
$this->assertCount(self::RECORDS_COUNT + 1, $repository->findAll());
}
/**
* @throws Exception
*/
public function testExistingInsert(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::EXISTING_DATE),
'workingTime' => new DateInterval(self::NEW_INTERVAL),
]);
$this->expectException(RepositoryModelAlreadyExists::class);
$repository->insert($model);
}
/**
* @throws RepositoryRecordNotFoundException
* @throws Exception
*/
public function testDeleteWithInvalidModel(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->makeEmpty(ModelInterface::class);
$this->expectException(InvalidArgumentException::class);
$repository->delete($model);
}
/**
* @throws RepositoryRecordNotFoundException
* @throws Exception
*/
public function testExistsDelete(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::EXISTING_DATE),
'workingTime' => new DateInterval(self::EXISTING_INTERVAL),
]);
$repository->delete($model);
$this->expectException(RepositoryRecordNotFoundException::class);
$repository->findByKey(self::EXISTING_DATE);
}
/**
* @throws RepositoryRecordNotFoundException
* @throws Exception
*/
public function testNotExistsDelete(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::NEW_DATE),
'workingTime' => new DateInterval(self::NEW_INTERVAL),
]);
$this->expectException(RepositoryRecordNotFoundException::class);
$repository->delete($model);
}
/**
* @throws RepositoryRecordNotFoundException
* @throws Exception
*/
public function testUpdateWithInvalidModel(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->makeEmpty(ModelInterface::class);
$this->expectException(InvalidArgumentException::class);
$repository->update($model);
}
/**
* @throws RepositoryRecordNotFoundException
* @throws Exception
*/
public function testExistsUpdate(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::EXISTING_DATE),
'workingTime' => new DateInterval(self::NEW_INTERVAL),
]);
$repository->update($model);
$model_db = $repository->findByKey(self::EXISTING_DATE);
$this->assertEquals(new DateInterval(self::NEW_INTERVAL), $model_db->getWorkingTime());
}
/**
* @throws Exception
*/
public function testNotExistsUpdate(): void
{
$repository = new WorkingHoursRepository($this->pdoObject);
$model = $this->make(WorkingHours::class, [
'workingDay' => new DateTime(self::NEW_DATE),
'workingTime' => new DateInterval(self::NEW_INTERVAL),
]);
$this->expectException(RepositoryRecordNotFoundException::class);
$repository->update($model);
}
}
@@ -0,0 +1,110 @@
<?php
namespace TorstenHettstedt\TimekeepingApi\Tests\Unit\Repositories;
use Codeception\Test\Unit;
use DateTime;
use DateTimeZone;
use Exception;
use PDO;
use TorstenHettstedt\TimekeepingApi\Models\WorkingHoursView;
use TorstenHettstedt\TimekeepingApi\Repositories\RepositoryRecordNotFoundException;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursWeeklyViewRepository;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursMonthlyViewRepository;
use TorstenHettstedt\TimekeepingApi\Repositories\WorkingHoursYearlyViewRepository;
class WorkingHoursViewRepositoryTest extends Unit
{
protected PDO $pdoObject;
protected function _before(): void
{
/** @noinspection SpellCheckingInspection */
$this->pdoObject = new PDO('pgsql:host=psql.torsten-hettstedt.net;port=5432;dbname=testdb;user=bruce;password=mypass');
parent::_before();
}
/**
* @return array[]
*/
public function listObjectProvider(): array
{
return [
[WorkingHoursYearlyViewRepository::class, 1],
[WorkingHoursMonthlyViewRepository::class, 3],
[WorkingHoursWeeklyViewRepository::class, 13],
];
}
/**
* @return array[]
* @throws Exception
*/
public function existingObjectProvider(): array
{
return [
[WorkingHoursYearlyViewRepository::class, '2020', 61, new DateTime('2020-01-01', new DateTimeZone('UCT'))],
[WorkingHoursMonthlyViewRepository::class, '2020 February', 20, new DateTime('2020-02-01', new DateTimeZone('UCT'))],
[WorkingHoursWeeklyViewRepository::class, '2020#03', 5, new DateTime('2020-01-13', new DateTimeZone('UCT'))],
];
}
/**
* @return array[]
*/
public function notExistingObjectProvider(): array
{
return [
[WorkingHoursYearlyViewRepository::class, '2022',],
[WorkingHoursMonthlyViewRepository::class, '2020 May',],
[WorkingHoursWeeklyViewRepository::class, '2020#30',],
];
}
/**
* @param string $period_class
* @param int $count_records
*
* @dataProvider listObjectProvider
*/
public function testFindAll(string $period_class, int $count_records): void
{
$repository = new $period_class($this->pdoObject);
$models = $repository->findAll();
$this->assertIsArray($models);
$this->assertContainsOnly(WorkingHoursView::class, $models);
$this->assertCount($count_records, $models);
}
/**
*
* @dataProvider existingObjectProvider
*
* @param string $period_class
* @param string $search
* @param int $workingDays
* @param DateTime $period
*/
public function testExistingFindByKey(string $period_class, string $search, int $workingDays, DateTime $period): void
{
$repository = new $period_class($this->pdoObject);
$model = $repository->findByKey($search);
$this->assertInstanceOf(WorkingHoursView::class, $model);
$this->assertEquals($workingDays, $model->getWorkingDays());
$this->assertEquals($period->format('Ymd'), $model->getPeriod()->format('Ymd'));
}
/**
* @param string $period_class
* @param string $search
*
* @dataProvider notExistingObjectProvider
*
*/
public function testNotExistingFindByKey(string $period_class, string $search): void
{
$repository = new $period_class($this->pdoObject);
$this->expectException(RepositoryRecordNotFoundException::class);
$repository->findByKey($search);
}
}