diff --git a/.gitignore b/.gitignore
index 7a91d23..ba38f8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
/scratch
.phpcs-cache
*~undo-tree~
+/app/tests/data/output
diff --git a/README.org b/README.org
index f3c6ca3..0443fbc 100644
--- a/README.org
+++ b/README.org
@@ -2,7 +2,7 @@
Automate generating invoices from youtrack reports and other time-tracking
related functionality.
-
+
** Usage
~./rprt.php invoice -y -p -s~
@@ -14,15 +14,14 @@
Asks which report to print from the list of your reports and then prints out
a table with that report.
-
+
** Install/Getting started
- Get ~.phar~ file from r, run configuration wizzard (planned for version 1.0).
-
+ Get ~.phar~ file from r, Run configuration wizzard (planned for version 1.0).
Before version 1.0 is released you have to navigate into ~/app~ directory
and call command through ~.rprt.php~ file.
-
+
*** Requirements
1. You have to create a youtrack API token.
@@ -45,7 +44,6 @@
- project categories
- hourly wage
- folder for reports
-
** Components
- report service - provides a csv of a report
@@ -61,7 +59,7 @@
*** ValueObjects
-
+
*** List my reports
#+begin_example bash
@@ -76,21 +74,34 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
I'd be using this app. If it get's picked up, by 5 others - it is worth
5 days of development.
- remove errors from reports
-
+
+* Development
+
+** Testing
+
+Main source at symfony [[https://symfony.com/doc/current/console.html#testing-commands][console command page]]. If you read thouroughly:
+
+When using the Console component in a standalone project, use Application and
+extend the normal \PHPUnit\Framework\TestCase.
+
** Plan
*** most current
1. For version 0.6.7
- [X] data value objects
- - phar file
+ - [X] phar file
- [X] additional expenses
2. For version 1.0
- plugin system
- for time tracking services
- for reports
- symfony/config & symfony/di components
+ 3. Time tracking service
+ - youtrack-api
+ - jira-api
+ - kimai (https://www.kimai.org/documentation/timesheet.html)
-*** current
+*** current
1. For Version 1.0
- Track Command (track time from your cli)
@@ -106,7 +117,7 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
- Add tests
-*** old
+*** old
1. Basic structure of the cli-app
1. App preparation
- nice specifications
@@ -124,7 +135,6 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
3. Second round of enhancements
1. Invoice output
2. configuration wizard
-
** Learning
@@ -134,8 +144,8 @@ curl 'https://drunomics.myjetbrains.com/youtrack/api/reports?$top=-1&fields=id,n
** API calls
-
*** Get csv file
~curl 'https://drunomics.myjetbrains.com/youtrack/api/reports/83-554/export/csv?&$top=-1' -H 'Accept: application/json, text/plain, */*' -H 'Accept-Language: en-US,en;q=0.5' --compressed -H "Authorization: Bearer $TKN" > ~/Documents/Drunomics/workhours/2021/21-09.csv~
+
diff --git a/app/composer.json b/app/composer.json
index 6783f0c..81012fc 100644
--- a/app/composer.json
+++ b/app/composer.json
@@ -24,14 +24,15 @@
},
"autoload": {
"psr-4": {
- "RprtCli\\": "src"
+ "RprtCli\\": ["src", "tests"]
}
},
"require-dev": {
"squizlabs/php_codesniffer": "^3.7",
"phpunit/phpunit": "^9.5",
"opsway/psr12-strict-coding-standard": "^0.5.0",
- "phpcompatibility/php-compatibility": "^9.3"
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpstan/phpstan": "^1.9"
},
"scripts": {
"cs": "phpcs --colors --standard=PSR12",
@@ -41,5 +42,6 @@
"allow-plugins": {
"dealerdirect/phpcodesniffer-composer-installer": true
}
- }
+ },
+ "minimum-stability": "alpha"
}
diff --git a/app/composer.lock b/app/composer.lock
index ee8d9fe..eb35a5a 100644
--- a/app/composer.lock
+++ b/app/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "7b0be09c8f282dfcceb075d1455caa28",
+ "content-hash": "eb0c874610e45fa74d8ccc691f5bd9f8",
"packages": [
{
"name": "doctrine/lexer",
@@ -3287,6 +3287,65 @@
"description": "PHPDoc parser with support for nullable, intersection and generic types",
"time": "2020-08-03T20:32:43+00:00"
},
+ {
+ "name": "phpstan/phpstan",
+ "version": "1.9.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpstan.git",
+ "reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/d03bccee595e2146b7c9d174486b84f4dc61b0f2",
+ "reference": "d03bccee595e2146b7c9d174486b84f4dc61b0f2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2|^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan-shim": "*"
+ },
+ "bin": [
+ "phpstan",
+ "phpstan.phar"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPStan - PHP Static Analysis Tool",
+ "keywords": [
+ "dev",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpstan/phpstan/issues",
+ "source": "https://github.com/phpstan/phpstan/tree/1.9.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/ondrejmirtes",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/phpstan",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpstan/phpstan",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-12-17T13:33:52+00:00"
+ },
{
"name": "phpunit/php-code-coverage",
"version": "9.2.15",
@@ -4930,7 +4989,7 @@
}
],
"aliases": [],
- "minimum-stability": "stable",
+ "minimum-stability": "alpha",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
diff --git a/app/phpcs.xml b/app/phpcs.xml
index 5700a1b..7264bbf 100644
--- a/app/phpcs.xml
+++ b/app/phpcs.xml
@@ -7,7 +7,7 @@
-
+
diff --git a/app/src/Commands/InvoiceCommand.php b/app/src/Commands/InvoiceCommand.php
index 760f78c..286f924 100644
--- a/app/src/Commands/InvoiceCommand.php
+++ b/app/src/Commands/InvoiceCommand.php
@@ -38,11 +38,13 @@ use function var_export;
/**
* Main file - invoice command.
*/
-class InvoiceCommand extends Command
-{
+class InvoiceCommand extends Command {
+
+ use SelectReportTrait;
+
protected $csv;
- protected $configuration;
+ protected $config;
protected $youtrack;
@@ -54,14 +56,14 @@ class InvoiceCommand extends Command
public function __construct(
ReportCsvInterface $csv,
ConfigurationInterface $configuration,
- YoutrackInterface $youtrack,
+ YoutrackInterface $trackingService,
PdfExportInterface $pdf_export,
MailerInterface $mailer,
?string $name = null
) {
$this->csv = $csv;
- $this->configuration = $configuration;
- $this->youtrack = $youtrack;
+ $this->config = $configuration;
+ $this->trackingService = $trackingService;
$this->pdfExport = $pdf_export;
$this->mailer = $mailer;
parent::__construct($name);
@@ -82,12 +84,12 @@ class InvoiceCommand extends Command
InputOption::VALUE_REQUIRED,
'Specify the input csv file to generate report from.'
);
- $this->addOption(
- 'youtrack',
- 'y',
- InputOption::VALUE_NONE,
- 'Use youtrack api to get a report. If this option is used --file does not have any effect..'
- );
+ // $this->addOption(
+ // 'youtrack',
+ // 'y',
+ // InputOption::VALUE_NONE,
+ // 'Use youtrack api to get a report. If this option is used --file does not have any effect..'
+ // );
$this->addOption(
'pdf',
'p',
@@ -150,71 +152,42 @@ class InvoiceCommand extends Command
protected function execute(InputInterface $input, OutputInterface $output) : int
{
if ($input->getOption('test')) {
- $test = $this->youtrack->testYoutrackapi();
+ $test = $this->trackingService->testYoutrackapi();
$output->writeln($test);
}
if ($input->getOption('list-reports')) {
- $list = $this->youtrack->listReports();
+ $list = $this->trackingService->listReports();
$output->writeln(var_export($list, true));
return Command::SUCCESS;
}
- if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
- if ($report = $input->getOption('report')) {
- $this->youtrack->setReportId($report);
- } else {
- $reports = $this->youtrack->listReports();
- $count = 1;
- foreach ($reports as $id => $name) {
- $output->writeln("[{$count}] {$name} ({$id})");
- $count++;
- }
- $report = readline('Select id of the report: ');
- // Asume people are literate.
- $this->youtrack->setReportId($report);
- }
- if ($output->isVerbose()) {
- $output->writeln("Setting report: {$report}.");
- }
- }
- if ($youtrack = $input->getOption('youtrack')) {
- $report_id = $this->youtrack->getReportId();
- $cache_clear_status = $this->youtrack->clearReportCache($report_id);
- if ($output->isVerbose()) {
- $output->writeln("Report {$report_id} cache cleared, status: {$cache_clear_status}");
- }
- $file = $this->youtrack->downloadReport($report_id);
- }
+ // Gets report parameter.
+ $this->getReportParameter($input, $output, 'tracking_service.youtrack.invoice.report');
+ $report_id = $this->trackingService->getReportId();
+ $file = $this->getReportCsvFilePath($input, $output, $report_id);
+ $report_name = $this->trackingService->getReportName();
if ($input->hasParameterOption('--expenses') || $input->hasParameterOption('-e')) {
$expenses = $this->getCustomWorkOrExpenses($input->getOption('expenses'), self::TYPE_EXPENSE);
}
if ($input->hasParameterOption('--custom') || $input->hasParameterOption('-c')) {
+ // @TODO Add option for custom time tracking data.
$custom = $this->getCustomWorkOrExpenses($input->getOption('custom'), self::TYPE_WORK);
}
- if ($youtrack || $file = $input->getOption('file')) {
- // Youtrack can also provide a file name.
- if ($output->isVerbose()) {
- $output->writeln("Csv file downloaded to: {$file}");
+ $output->writeln("report: {$report_name}");
+ $data = $this->csv->getInvoiceData($file);
+ if (! empty($expenses)) {
+ $data = array_merge($data, $expenses);
+ }
+ $table = $this->getTable($output, $data);
+ $table->render();
+ if ($input->getOption('pdf')) {
+ $nice_data = $this->csv->arangeDataForDefaultPdfExport($data);
+ // @TODO method gatherTokens();
+ if ($out = $input->getOption('output')) {
+ $this->pdfExport->setOutput($out);
}
- $data = $this->csv->getInvoiceData($file);
- if (! empty($expenses)) {
- $data = array_merge($data, $expenses);
- }
- // $table = $this->generateTable($output, $data);
- $table = $this->getTable($output, $data);
- $table->render();
-
- if ($input->getOption('pdf')) {
- $nice_data = $this->csv->arangeDataForDefaultPdfExport($data);
- // @TODO method gatherTokens();
- if ($out = $input->getOption('output')) {
- $this->pdfExport->setOutput($out);
- }
- $output_path = $this->pdfExport->fromDefaultDataToPdf($nice_data);
- // Notify the user where the file was generated to.
- $output->writeln("The file was generated at ${output_path}.");
- }
-
- // return Command::SUCCESS;
+ $output_path = $this->pdfExport->fromDefaultDataToPdf($nice_data);
+ // Notify the user where the file was generated to.
+ $output->writeln("The file was generated at ${output_path}.");
}
if ($input->getOption('send') && $output_path) {
// @TODO If no output path print an error.
@@ -267,7 +240,7 @@ class InvoiceCommand extends Command
'Price',
]);
[$rows, $totalHours, $totalPrice] = [[], 0, 0];
- $projectsConfig = $this->configuration->get('projects');
+ $projectsConfig = $this->config->get('projects');
foreach ($projectsConfig as $name => $config) {
if (! isset($data[$name])) {
// @TODO Proper error handling.
@@ -353,8 +326,7 @@ class InvoiceCommand extends Command
return $output;
}
- protected function getCustomWorkOrExpenses($custom, $type)
- {
+ protected function getCustomWorkOrExpenses($custom, $type) {
$output = [];
if (is_string($custom)) {
foreach (explode(';', $custom) as $item) {
diff --git a/app/src/Commands/ReportCommand.php b/app/src/Commands/ReportCommand.php
index 111b4df..f61d56d 100644
--- a/app/src/Commands/ReportCommand.php
+++ b/app/src/Commands/ReportCommand.php
@@ -14,14 +14,14 @@ use Symfony\Component\Console\Helper\TableSeparator;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Question\ChoiceQuestion;
-use function array_flip;
use function is_array;
use function is_null;
-class ReportCommand extends Command
-{
+class ReportCommand extends Command {
+
+ use SelectReportTrait;
+
protected $trackingService;
protected $config;
@@ -37,22 +37,30 @@ class ReportCommand extends Command
parent::__construct($name);
}
- protected function configure() : void
- {
+ protected function configure() : void {
$this->setName('report');
$this->setDescription('Get a time-tracking report into command line.');
$this->addOption(
'report',
'r',
InputOption::VALUE_OPTIONAL,
- 'Select a report from list of your reports'
+ 'Select a report from list ofo your reports'
);
+ // Not supported by by ReportCsv service!
+ // @TODO Build factory for time tracking service (youtrack-csv-export,
+ // youtrack-api, jira-api, kimai-api (biro.radiostudent.si)).
$this->addOption(
'time-range',
't',
InputOption::VALUE_REQUIRED,
'Calculates report from tracking service work items directly for time range'
);
+ $this->addOption(
+ 'file',
+ 'f',
+ InputOption::VALUE_REQUIRED,
+ 'Specify the input csv file to generate report from.'
+ );
}
protected function execute(InputInterface $input, OutputInterface $output) : int
@@ -61,50 +69,19 @@ class ReportCommand extends Command
// @TODO: Implement time range option:
// - Request workTime items from tracking service
// - Filter them, join by issue, project ...
+ // This will came in other report service that will gather data
+ // directly from cache.
if ($output->isVerbose()) {
$output->writeln("Time range: {$timeRange}");
}
$output->writeln('This option is not supported yet.');
return Command::FAILURE;
}
- $reports = $this->trackingService->listReports();
- // Could just parse a csv file or actually get workItems from youtrack ...
- if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
- if (! $report = $input->getOption('report')) {
- // @TODO Make selection nicer.
- // QuestionHelper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html
- $helper = $this->getHelper('question');
- $question = new ChoiceQuestion('Select report:', $reports);
- $question
- ->setErrorMessage('Report %s does not exist!')
- ->setAutocompleterValues($reports);
- $report = $helper->ask($input, $output, $question);
- $output->writeln('Report ' . $report . ' selected.');
- }
- // If parameter option is not recognised check if report name was given.
- if (! isset($reports[$report])) {
- if (! isset(array_flip($reports)[$report])) {
- $output->writeln('Non-existing report ' . $report . '. Exiting.');
- return Command::SUCCESS;
- }
- $report = array_flip($reports)[$report];
- }
- $this->trackingService->setReportId($report);
- } elseif ($report = $this->config->get('tracking_service.youtrack.report.default')) {
- $this->trackingService->setReportId($report);
- }
+ $this->getReportParameter($input, $output);
// Currently we only support csv download.
$report_id = $this->trackingService->getReportId();
- $report_name = $reports[$report_id];
- // Code duplication.
- $cache_clear_status = $this->trackingService->clearReportCache($report_id);
- if ($output->isVerbose()) {
- $output->writeln("Report cache cleared, status: {$cache_clear_status}");
- }
- $file = $this->trackingService->downloadReport($report_id);
- if ($output->isVerbose()) {
- $output->writeln("Csv file downloaded to: {$file}");
- }
+ $file = $this->getReportCsvFilePath($input, $output, $report_id);
+ $report_name = $this->trackingService->getReportName();
$output->writeln("report: {$report_name}");
$data = $this->csv->generateReportTable($file);
$table = $this->buildTable($output, $data);
diff --git a/app/src/Commands/SelectReportTrait.php b/app/src/Commands/SelectReportTrait.php
new file mode 100644
index 0000000..bcf26bf
--- /dev/null
+++ b/app/src/Commands/SelectReportTrait.php
@@ -0,0 +1,80 @@
+trackingService)) {
+ return FALSE;
+ }
+ return $this instanceof Command;
+ }
+
+ protected function getReportParameter(InputInterface $input, OutputInterface $output, $default = 'tracking_service.youtrack.report.report') {
+ if (!$this->checkContext()) {
+ return Command::FAILURE;
+ }
+ $reports = $this->trackingService->listReports();
+ // Could just parse a csv file or actually get workItems from youtrack ...
+ if ($input->hasParameterOption('--report') || $input->hasParameterOption('-r')) {
+ if (! $report_id = $input->getOption('report')) {
+ // @TODO Make selection nicer.
+ // QuestionHelper: https://symfony.com/doc/current/components/console/helpers/questionhelper.html
+ $helper = $this->getHelper('question');
+ $question = new ChoiceQuestion('Select report:', $reports);
+ $question
+ ->setErrorMessage('Report %s does not exist!')
+ ->setAutocompleterValues($reports);
+ $report_id = $helper->ask($input, $output, $question);
+ $output->writeln('Report ' . $report_id . ' selected.');
+ }
+ // If parameter option is not recognised check if report name was given.
+ if (! isset($reports[$report_id])) {
+ if (! isset(array_flip($reports)[$report_id])) {
+ $output->writeln('Non-existing report ' . $report_id . '. Exiting.');
+ return Command::FAILURE;
+ }
+ $report_id = array_flip($reports)[$report_id];
+ }
+ $this->trackingService->setReportId($report_id);
+ } elseif ($report_id = $this->config->get($default)) {
+ // If report is not set, try getting default report from configuration.
+ // This is dependant on the config.
+ $this->trackingService->setReportId($report_id);
+ }
+ $this->trackingService->setReportName();
+ }
+
+ protected function getReportCsvFilePath(InputInterface $input, OutputInterface $output, ?string $report_id) : ?string {
+ if (!$file = $input->getOption('file')) {
+ $cache_clear_status = $this->trackingService->clearReportCache($report_id);
+ if ($output->isVerbose()) {
+ $output->writeln("Report {$report_id} cache cleared, status: {$cache_clear_status}");
+ }
+ $file = $this->trackingService->downloadReport($report_id);
+ if ($output->isVerbose()) {
+ $output->writeln("Csv file downloaded to: {$file}");
+ }
+ } else {
+ $this->trackingService->setReportName($file);
+ }
+ return $file;
+ }
+
+}
+
diff --git a/app/src/Utils/CsvReport/ReportCsv.php b/app/src/Utils/CsvReport/ReportCsv.php
index befef19..d17823a 100644
--- a/app/src/Utils/CsvReport/ReportCsv.php
+++ b/app/src/Utils/CsvReport/ReportCsv.php
@@ -58,7 +58,7 @@ class ReportCsv implements ReportCsvInterface
if ($file = fopen($filePath, 'r')) {
while (($line = fgetcsv($file)) !== false) {
$parsed = $this->parseCsvFile($line);
- // $key = reset(array_keys($parsed));
+ // $key = reset(array_keys($parsed));
$key = array_key_first($parsed);
if (isset($output[$key])) {
$output[$key] += (float) reset($parsed);
@@ -188,10 +188,11 @@ class ReportCsv implements ReportCsvInterface
if (empty($data)) {
return [];
}
- [$previous, $time_sum, $project_time, $table, $all_projects] = [$data[0]['id'], 0, 0, [], []];
+ $explodeMinus = fn($ticket) => explode('-', $ticket)[0] ?? 'UNKNOWN';
+ [$previous, $time_sum, $project_time, $table, $all_projects] = [$data[0]['id'], 0, 0, [], [$explodeMinus($data[0]['id'])]];
foreach ($data as $line) {
- $project = explode('-', $line['id'])[0];
- $previous_project = explode('-', $previous)[0];
+ $project = $explodeMinus($line['id']);
+ $previous_project = $explodeMinus($previous);
if ($project !== $previous_project) {
// When project changes, add a sum of time for that project.
$table[] = ReportCsvInterface::SEPARATOR_MEDIUM;
@@ -242,32 +243,4 @@ class ReportCsv implements ReportCsvInterface
return $output;
}
- /**
- * Should be moved into test class.
- */
- protected function dummyConfig() : array
- {
- return [
- 'projects' => [
- 'LDP' => [
- 'name' => 'lupus.digital',
- 'pattern' => 'LDP-[0-9]+',
- 'price' => 25,
- // optional specify columns
- ],
- 'WV' => [
- 'name' => 'Wirtschaftsverlag',
- 'pattern' => 'WV-[0-9]+',
- 'price' => 25,
- // optional specify columns
- ],
- 'Other' => [
- 'name' => 'Other projects',
- 'pattern' => '(?!.\bLDP\b)(?!.\bWV\b)',
- 'price' => 25,
- // optional specify columns
- ],
- ],
- ];
- }
}
diff --git a/app/src/Utils/TimeTrackingServices/YoutrackInterface.php b/app/src/Utils/TimeTrackingServices/YoutrackInterface.php
index 5230e47..1caa15b 100644
--- a/app/src/Utils/TimeTrackingServices/YoutrackInterface.php
+++ b/app/src/Utils/TimeTrackingServices/YoutrackInterface.php
@@ -34,4 +34,8 @@ interface YoutrackInterface
* Array of reports with ids as keys and names as values.
*/
public function listReports() : array;
+
+ public function setReportId(string $report_id) :void;
+ public function setReportName(?string $report_name = NULL) :void;
+ public function getReportName() : ?string;
}
diff --git a/app/src/Utils/TimeTrackingServices/YoutrackService.php b/app/src/Utils/TimeTrackingServices/YoutrackService.php
index 9b3b992..c383051 100644
--- a/app/src/Utils/TimeTrackingServices/YoutrackService.php
+++ b/app/src/Utils/TimeTrackingServices/YoutrackService.php
@@ -28,14 +28,15 @@ use function var_dump;
class YoutrackService implements YoutrackInterface
{
- protected $ytToken;
- protected $ytBaseUrl;
+ protected string $ytToken;
+ protected string $ytBaseUrl;
- protected $config;
+ protected ConfigurationInterface $config;
- protected $httpClient;
+ protected ClientInterface $httpClient;
- protected $report_id;
+ protected string $report_id;
+ protected ?string $reportName = NULL;
public function __construct(ConfigurationInterface $config, ClientInterface $http_client)
{
@@ -106,6 +107,23 @@ class YoutrackService implements YoutrackInterface
$this->report_id = $report_id;
}
+ public function setReportName(?string $report_name = NULL) : void {
+ if (!$report_name) {
+ $reports = $this->listReports();
+ if ($report_id = $this->getReportId()) {
+ $report_name = $reports[$report_id] ?? NULL;
+ }
+ else {
+ $report_name = NULL;
+ }
+ }
+ $this->reportName = $report_name;
+ }
+
+ public function getReportName() : ?string {
+ return $this->reportName;
+ }
+
public function downloadReport(string $report_id) : ?string
{
$path = "youtrack/api/reports/$report_id/export/csv";
diff --git a/app/tests/Kernel/ReportCommandTest.php b/app/tests/Kernel/ReportCommandTest.php
new file mode 100644
index 0000000..cb34dea
--- /dev/null
+++ b/app/tests/Kernel/ReportCommandTest.php
@@ -0,0 +1,50 @@
+addDefinitions(__DIR__ . '/../test-dependencies.php');
+ $container = $builder->build();
+
+ $application = new Application('Command Line Tool to process Youtrack Reports', '0.1.0');
+
+ $invoiceCommand = $container->get(InvoiceCommand::class);
+ $application->add($invoiceCommand);
+ $reportCommand = $container->get(ReportCommand::class);
+ $application->add($reportCommand);
+
+
+ $command = $application->find('report');
+ $commandTester = new CommandTester($command);
+ $commandTester->execute([
+ // pass arguments to the helper
+ '--file' => __DIR__ . '/../data/21-03.csv',
+
+ // prefix the key with two dashes when passing options,
+ // e.g: '--some-option' => 'option_value',
+ // use brackets for testing array value,
+ // e.g: '--some-option' => ['option_value'],
+ ]);
+
+ $commandTester->assertCommandIsSuccessful();
+
+ // the output of the command in the console
+ $output = $commandTester->getDisplay();
+ var_dump($output);
+ $this->assertStringContainsString('21-03.csv', $output);
+
+ // ...
+ }
+}
diff --git a/app/tests/data/21-03.csv b/app/tests/data/21-03.csv
new file mode 100644
index 0000000..6d360bf
--- /dev/null
+++ b/app/tests/data/21-03.csv
@@ -0,0 +1,53 @@
+Group name,Item,Item Summary,Estimation time,Spent time
+-,DEV-46,"Wochenplanung, Weekly",0,315
+-,DEV-56,Developer training,0,60
+-,DEV-122,Daily standup,0,255
+-,INF-6,"Infrastructure misc, diverse",0,75
+-,KUR-181,Sprint meetings (PM),240,390
+-,KUR-507,(TKT-307) Change webhook integration for Slack,0,330
+-,KUR-552,(TKT-740) Allow kicking users out of collections,780,45
+-,KUR-554,(TKT-1448) Adaptions in Backend,0,30
+-,KUR-556,[story] (TKT-1393) Make Paragraph embedding more user friendly,0,75
+-,KUR-558,(TKT-1467) Full copy of article doesn't update the timestamp,0,15
+-,KUR-562,(TKT-1441) Implement SEO title in BE,0,45
+-,LDP-668,Add json-ld schema.org metadata to articles,0,300
+-,LDP-685,"LDP-Contentpool sitemaps. Extend ""simple_sitemap_extended"" setup to support per front-end site sitemaps.",0,15
+-,LDP-692,Output json-ld script,0,30
+-,LDP-693,Add WebPage schema.org support,120,180
+-,LDP-700,LDP onboarding Somebody,0,15
+-,LDP-706,Backend FAQ Marketing Block,0,165
+-,LDP-707,"Backend ""Hero"" Marketing Block",0,1035
+-,LDP-709,"Backend ""Newsletter Signup"" Marketing Block",0,195
+-,LDP-711,"Backend ""Call to action"" Marketing Block",0,165
+-,LDP-712,"Backend ""Feature section"" Marketing Block",0,300
+-,LDP-713,Provide a field type and pre-configuration for choosing icons,300,45
+-,LDP-716,Entity browser UX is not as good as expected,0,15
+-,LDP-720,Improve display of nodes and add support for article heros,0,45
+-,LDP-728,ldp-marketing blocks break the page when they are added to the layout builder,0,195
+-,LDP-729,ldp-cp portal entity,0,480
+-,LDP-740,CP: Add portal path prefixes,480,30
+-,LDP-741,Defects marketing Blocks,0,450
+-,LDP-742,[Red Alert] Develop branch fails to build,0,45
+-,VAL-8,Plan hosting setup on custom hosting provider,0,45
+-,WV-31,"Internal sprint meetings (PM, fixed-price).",0,150
+-,WV-4304,"(ODT-1240) Structured data on article (satellite) pages and overview pages such as the startpage, branch, topic & channel pages.",600,750
+-,WV-4305,(ODT-1202) Add mobile-banner-3 & 4 (advertisement in articles),0,60
+-,WV-4326,(ODT-1174) Make image optional in the Call to Action block,135,60
+-,WV-4355,(ODT-1270) Add article title to breadcrumbs,0,270
+-,WV-4359,"(ODT-1254) Display the text paragraph ""zwischentitel"" in H2",0,90
+-,WV-4372,(ODT-1297) Branch analytics for articles associated with specific branch (GA),135,15
+-,WV-4384,(ODT-1306) Layout-builder rendering in Chrome browser,120,120
+-,WV-4393,WVsat docker build error,0,90
+-,WV-4410,(ODT-1317) SOLR Search optimization >> suggested articles does NOT affect the relevance for search results.,375,30
+-,WV-4411,Extend simple_sitemap_extensions for dynamic variants & Install and configure simple_sitemap_extensions,600,2070
+-,WV-4412,Implement custom dynamic variants for sitemaps to group by month,0,105
+-,WV-4414,Invalid JSON in the cache_tools composer.json file,0,60
+-,WV-4421,WV Satellites Hand bau image file entities are not replicated,0,15
+-,WV-4422,"[Red Alert] Behat test fail. ""Demo content loaded..Error pages work.""",0,75
+-,WV-4423,(ODT-1325) Site managers must be able to change article status from Unpublished to Unpublished,0,15
+-,WV-4425,Unpublished Advertorials are returned,0,30
+-,WV-4427,"(ODT-1332) ""Og: image"" is not specified explicitly",120,15
+-,WV-4437,"(ODT-1339) User (id 511) on wv-contentpool is locked and cannot access the system.",0,30
+-,WV-4442,(ODT-1341) Custom HTML block is not rendered,0,120
+-,WV-4478,(ODT-1368) Hosting provider transition of all projects hosted on their OpenShift infrastructure to the new Kuberneets (k8) infrastructure.,0,90
+-,WV-4480,(ODT-1370) Error: Call to a member function getUrl() on null / cannot save an article on stage systems,0,90
diff --git a/app/tests/data/email-template.txt b/app/tests/data/email-template.txt
new file mode 100644
index 0000000..97554c3
--- /dev/null
+++ b/app/tests/data/email-template.txt
@@ -0,0 +1,10 @@
+Hello,
+
+I'm sending you the invoice for [[month]] [[year]].
+
+This email and invoice were generated and sent by (rprt-cli)[https://git.kompot.si/lio/RprtCli] (WIP).
+
+
+Kind regards
+
+Liopold Novelli
diff --git a/app/tests/data/invoice-template.html b/app/tests/data/invoice-template.html
new file mode 100644
index 0000000..24828f5
--- /dev/null
+++ b/app/tests/data/invoice-template.html
@@ -0,0 +1,91 @@
+
+
+
+
+
+ Spletni razvoj, Liopold Doron Novelli s.p.,
ADDRESS
NUMBER, liopold@drunomics.com
NUMBER
+
+ Company name
Company Name
Wien
Austria
+
+
+
+ Laibach, am [[today]]
+ Honorarnote
Nummer [[number]]
+
+ Für meine Tätigkeit Programmieren von [[date_start]] bis [[date_end]] erlaube ich mir, folgenden Betrag in Rechnung zu stellen:
+
+ [[table]]
+
Ich ersuche Sie höflich, den oben angeführt auf meine Kontonummer ACCOUNT NUMBER mit der Bankleitzahl BANK zu überweisen.
+
Vielen Dank für den Auftrag,
+ mit besten Grüßen,
+
+
+ Liopold Doron Novelli
+
+
diff --git a/app/tests/data/rprt.config.yml b/app/tests/data/rprt.config.yml
new file mode 100644
index 0000000..3e8d717
--- /dev/null
+++ b/app/tests/data/rprt.config.yml
@@ -0,0 +1,47 @@
+tracking_service:
+ youtrack:
+ auth_token: 'perm:'
+ base_url: 'https://.myjetbrains.com'
+ report_id: '83-554'
+ # report command
+ report:
+ report: '83-541'
+ # invoice command
+ invoice:
+ # default value for report parameter
+ report: '83-554'
+export:
+ template_path: 'tests/data/invoice-template.html'
+ output: 'tests/data/output/Invoice-Novelli-[[month]]-[[year]].pdf'
+ labels:
+ - 'Tätigkeit'
+ - 'Stunden'
+ - 'Stundensatz'
+ - 'EUR'
+email:
+ template_path: 'tests/data/rprt-cli/email-template.txt'
+ username: ''
+ tokens:
+ me: 'Liopold Novelli'
+ to:
+ - 'wolfgang.ziegler@drunomics.com'
+ - 'accounting@drunomics.com'
+ from: 'liopolddoron@gmail.com'
+projects:
+ LDP:
+ name: 'lupus.digital publishing'
+ pattern: 'LDP'
+ price: 25.6
+ currency: 'EUR'
+ project_column: 1
+ time_column: 4
+ # time format m - minutes, h - hours
+ time_format: 'm'
+ Other:
+ name: 'Projektbezogene Entwicklung'
+ pattern: '(?!.\bLDP\b)'
+ price: 29.1
+ currency: 'EUR'
+ project_column: 1
+ time_column: 4
+ time_format: 'm'
diff --git a/app/tests/test-dependencies.php b/app/tests/test-dependencies.php
new file mode 100644
index 0000000..3675163
--- /dev/null
+++ b/app/tests/test-dependencies.php
@@ -0,0 +1,7 @@
+