ソースを参照

initial release of FinFollowBCS

master
Evgeniy Ierusalimov 1年前
コミット
8948925ac9

+ 22
- 0
.env ファイルの表示

@@ -18,3 +18,25 @@
18 18
 APP_ENV=dev
19 19
 APP_SECRET=433b91b597b7ee4854b62c68fd2f5174
20 20
 ###< symfony/framework-bundle ###
21
+
22
+##
23
+IMAP_PATH=
24
+IMAP_USERNAME=
25
+IMAP_STORE_ATTACHMENTS_DIR="%kernel.project_dir%/var/imap/attachments"
26
+
27
+###> doctrine/doctrine-bundle ###
28
+# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
29
+# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
30
+#
31
+# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
32
+# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
33
+# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
34
+DATABASE_URL="postgresql://db_user:secret@127.0.0.1:5432/finfollow?serverVersion=11&charset=utf8"
35
+###< doctrine/doctrine-bundle ###
36
+
37
+###> symfony/telegram-notifier ###
38
+TELEGRAM_DSN=telegram://bot_token@default?channel=channel_id
39
+###< symfony/telegram-notifier ###
40
+
41
+TELEGRAM_BOT_TOKEN=
42
+TELEGRAM_CHAT_ID=

+ 10
- 0
composer.json ファイルの表示

@@ -7,14 +7,24 @@
7 7
         "php": ">=8.2",
8 8
         "ext-ctype": "*",
9 9
         "ext-iconv": "*",
10
+        "doctrine/dbal": "^3",
11
+        "doctrine/doctrine-bundle": "^2.12",
12
+        "doctrine/doctrine-migrations-bundle": "^3.3",
13
+        "doctrine/orm": "^3.2",
14
+        "secit-pl/imap-bundle": "^3.2",
10 15
         "symfony/console": "7.1.*",
11 16
         "symfony/dotenv": "7.1.*",
17
+        "symfony/filesystem": "7.1.*",
12 18
         "symfony/flex": "^2",
13 19
         "symfony/framework-bundle": "7.1.*",
20
+        "symfony/monolog-bundle": "^3.10",
21
+        "symfony/notifier": "7.1.*",
14 22
         "symfony/runtime": "7.1.*",
23
+        "symfony/telegram-notifier": "7.1.*",
15 24
         "symfony/yaml": "7.1.*"
16 25
     },
17 26
     "require-dev": {
27
+        "symfony/maker-bundle": "^1.59"
18 28
     },
19 29
     "config": {
20 30
         "allow-plugins": {

+ 3106
- 625
composer.lock
ファイル差分が大きすぎるため省略します
ファイルの表示


+ 5
- 0
config/bundles.php ファイルの表示

@@ -2,4 +2,9 @@
2 2
 
3 3
 return [
4 4
     Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
5
+    Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
6
+    SecIT\ImapBundle\ImapBundle::class => ['all' => true],
7
+    Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
8
+    Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
9
+    Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
5 10
 ];

+ 52
- 0
config/packages/doctrine.yaml ファイルの表示

@@ -0,0 +1,52 @@
1
+doctrine:
2
+    dbal:
3
+        url: '%env(resolve:DATABASE_URL)%'
4
+
5
+        # IMPORTANT: You MUST configure your server version,
6
+        # either here or in the DATABASE_URL env var (see .env file)
7
+        #server_version: '16'
8
+
9
+        profiling_collect_backtrace: '%kernel.debug%'
10
+        use_savepoints: true
11
+    orm:
12
+        auto_generate_proxy_classes: true
13
+        enable_lazy_ghost_objects: true
14
+        report_fields_where_declared: true
15
+        validate_xml_mapping: true
16
+        naming_strategy: doctrine.orm.naming_strategy.underscore_number_aware
17
+        auto_mapping: true
18
+        mappings:
19
+            App:
20
+                type: attribute
21
+                is_bundle: false
22
+                dir: '%kernel.project_dir%/src/Entity'
23
+                prefix: 'App\Entity'
24
+                alias: App
25
+        controller_resolver:
26
+            auto_mapping: false
27
+
28
+when@test:
29
+    doctrine:
30
+        dbal:
31
+            # "TEST_TOKEN" is typically set by ParaTest
32
+            dbname_suffix: '_test%env(default::TEST_TOKEN)%'
33
+
34
+when@prod:
35
+    doctrine:
36
+        orm:
37
+            auto_generate_proxy_classes: false
38
+            proxy_dir: '%kernel.build_dir%/doctrine/orm/Proxies'
39
+            query_cache_driver:
40
+                type: pool
41
+                pool: doctrine.system_cache_pool
42
+            result_cache_driver:
43
+                type: pool
44
+                pool: doctrine.result_cache_pool
45
+
46
+    framework:
47
+        cache:
48
+            pools:
49
+                doctrine.result_cache_pool:
50
+                    adapter: cache.app
51
+                doctrine.system_cache_pool:
52
+                    adapter: cache.system

+ 6
- 0
config/packages/doctrine_migrations.yaml ファイルの表示

@@ -0,0 +1,6 @@
1
+doctrine_migrations:
2
+    migrations_paths:
3
+        # namespace is arbitrary but should be different from App\Migrations
4
+        # as migrations classes should NOT be autoloaded
5
+        'DoctrineMigrations': '%kernel.project_dir%/migrations'
6
+    enable_profiler: false

+ 8
- 0
config/packages/imap.yaml ファイルの表示

@@ -0,0 +1,8 @@
1
+imap:
2
+    connections:
3
+        finfollow:
4
+            imap_path: "%env(IMAP_PATH)%"
5
+            username: "%env(IMAP_USERNAME)%"
6
+            password: "%env(IMAP_PASSWORD)%"
7
+            attachments_dir: "%env(IMAP_STORE_ATTACHMENTS_DIR)%"
8
+#            server_encoding: "UTF-8"

+ 62
- 0
config/packages/monolog.yaml ファイルの表示

@@ -0,0 +1,62 @@
1
+monolog:
2
+    channels:
3
+        - deprecation # Deprecations are logged in the dedicated "deprecation" channel when it exists
4
+
5
+when@dev:
6
+    monolog:
7
+        handlers:
8
+            main:
9
+                type: stream
10
+                path: "%kernel.logs_dir%/%kernel.environment%.log"
11
+                level: debug
12
+                channels: ["!event"]
13
+            # uncomment to get logging in your browser
14
+            # you may have to allow bigger header sizes in your Web server configuration
15
+            #firephp:
16
+            #    type: firephp
17
+            #    level: info
18
+            #chromephp:
19
+            #    type: chromephp
20
+            #    level: info
21
+            console:
22
+                type: console
23
+                process_psr_3_messages: false
24
+                channels: ["!event", "!doctrine", "!console"]
25
+
26
+when@test:
27
+    monolog:
28
+        handlers:
29
+            main:
30
+                type: fingers_crossed
31
+                action_level: error
32
+                handler: nested
33
+                excluded_http_codes: [404, 405]
34
+                channels: ["!event"]
35
+            nested:
36
+                type: stream
37
+                path: "%kernel.logs_dir%/%kernel.environment%.log"
38
+                level: debug
39
+
40
+when@prod:
41
+    monolog:
42
+        handlers:
43
+            main:
44
+                type: fingers_crossed
45
+                action_level: error
46
+                handler: nested
47
+                excluded_http_codes: [404, 405]
48
+                buffer_size: 50 # How many messages should be saved? Prevent memory leaks
49
+            nested:
50
+                type: stream
51
+                path: php://stderr
52
+                level: debug
53
+                formatter: monolog.formatter.json
54
+            console:
55
+                type: console
56
+                process_psr_3_messages: false
57
+                channels: ["!event", "!doctrine"]
58
+            deprecation:
59
+                type: stream
60
+                channels: [deprecation]
61
+                path: php://stderr
62
+                formatter: monolog.formatter.json

+ 13
- 0
config/packages/notifier.yaml ファイルの表示

@@ -0,0 +1,13 @@
1
+framework:
2
+    notifier:
3
+        chatter_transports:
4
+            telegram: '%env(TELEGRAM_DSN)%'
5
+        texter_transports:
6
+        channel_policy:
7
+            # use chat/slack, chat/telegram, sms/twilio or sms/nexmo
8
+            urgent: ['email']
9
+            high: ['email']
10
+            medium: ['email']
11
+            low: ['email']
12
+        admin_recipients:
13
+            - { email: admin@example.com }

+ 4
- 0
config/services.yaml ファイルの表示

@@ -22,3 +22,7 @@ services:
22 22
 
23 23
     # add more service definitions when explicit configuration is needed
24 24
     # please note that last definitions always *replace* previous ones
25
+    App\Service\TelegramNotifier:
26
+        arguments:
27
+            $telegramBotToken: '%env(TELEGRAM_BOT_TOKEN)%'
28
+            $telegramChatId: '%env(TELEGRAM_CHAT_ID)%'

+ 45
- 0
migrations/Version20240608120637.php ファイルの表示

@@ -0,0 +1,45 @@
1
+<?php
2
+
3
+declare(strict_types=1);
4
+
5
+namespace DoctrineMigrations;
6
+
7
+use Doctrine\DBAL\Schema\Schema;
8
+use Doctrine\Migrations\AbstractMigration;
9
+
10
+/**
11
+ * Auto-generated Migration: Please modify to your needs!
12
+ */
13
+final class Version20240608120637 extends AbstractMigration
14
+{
15
+    public function getDescription(): string
16
+    {
17
+        return '';
18
+    }
19
+
20
+    public function up(Schema $schema): void
21
+    {
22
+        // this up() migration is auto-generated, please modify it to your needs
23
+        $this->addSql('CREATE TABLE portfolio (id SERIAL NOT NULL, client_agreement VARCHAR(255) NOT NULL, xml_data TEXT NOT NULL, start_date DATE NOT NULL, end_date DATE NOT NULL, PRIMARY KEY(id))');
24
+        $this->addSql('COMMENT ON COLUMN portfolio.start_date IS \'(DC2Type:date_immutable)\'');
25
+        $this->addSql('COMMENT ON COLUMN portfolio.end_date IS \'(DC2Type:date_immutable)\'');
26
+        $this->addSql('CREATE TABLE portfolio_detail (id SERIAL NOT NULL, portfolio_id INT NOT NULL, security VARCHAR(32) NOT NULL, quantity_start INT NOT NULL, quantity_end INT NOT NULL, price_start NUMERIC(10, 3) NOT NULL, price_end NUMERIC(10, 3) NOT NULL, sum_start NUMERIC(10, 3) NOT NULL, sum_end NUMERIC(10, 3) NOT NULL, issuer VARCHAR(255) NOT NULL, PRIMARY KEY(id))');
27
+        $this->addSql('CREATE INDEX IDX_28ED92A6B96B5643 ON portfolio_detail (portfolio_id)');
28
+        $this->addSql('CREATE TABLE portfolio_movement (id SERIAL NOT NULL, portfolio_id INT NOT NULL, security VARCHAR(255) NOT NULL, period TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, quantity_start INT NOT NULL, quantity_end INT NOT NULL, quantity_income INT NOT NULL, quantity_outcome INT NOT NULL, PRIMARY KEY(id))');
29
+        $this->addSql('CREATE INDEX IDX_8857CF6FB96B5643 ON portfolio_movement (portfolio_id)');
30
+        $this->addSql('COMMENT ON COLUMN portfolio_movement.period IS \'(DC2Type:datetime_immutable)\'');
31
+        $this->addSql('ALTER TABLE portfolio_detail ADD CONSTRAINT FK_28ED92A6B96B5643 FOREIGN KEY (portfolio_id) REFERENCES portfolio (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
32
+        $this->addSql('ALTER TABLE portfolio_movement ADD CONSTRAINT FK_8857CF6FB96B5643 FOREIGN KEY (portfolio_id) REFERENCES portfolio (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
33
+    }
34
+
35
+    public function down(Schema $schema): void
36
+    {
37
+        // this down() migration is auto-generated, please modify it to your needs
38
+        $this->addSql('CREATE SCHEMA public');
39
+        $this->addSql('ALTER TABLE portfolio_detail DROP CONSTRAINT FK_28ED92A6B96B5643');
40
+        $this->addSql('ALTER TABLE portfolio_movement DROP CONSTRAINT FK_8857CF6FB96B5643');
41
+        $this->addSql('DROP TABLE portfolio');
42
+        $this->addSql('DROP TABLE portfolio_detail');
43
+        $this->addSql('DROP TABLE portfolio_movement');
44
+    }
45
+}

+ 55
- 0
src/Command/FetchEmailsCommand.php ファイルの表示

@@ -0,0 +1,55 @@
1
+<?php
2
+
3
+namespace App\Command;
4
+
5
+use App\Service\MailFetcher;
6
+use Symfony\Component\Console\Attribute\AsCommand;
7
+use Symfony\Component\Console\Command\Command;
8
+use Symfony\Component\Console\Input\InputArgument;
9
+use Symfony\Component\Console\Input\InputInterface;
10
+use Symfony\Component\Console\Input\InputOption;
11
+use Symfony\Component\Console\Output\OutputInterface;
12
+use Symfony\Component\Console\Style\SymfonyStyle;
13
+
14
+#[AsCommand(
15
+    name: 'app:fetch-emails',
16
+    description: 'Fetch emails and process them',
17
+)]
18
+class FetchEmailsCommand extends Command
19
+{
20
+    private MailFetcher $mailFetcher;
21
+
22
+    public function __construct(MailFetcher $mailFetcher)
23
+    {
24
+        parent::__construct();
25
+        $this->mailFetcher = $mailFetcher;
26
+    }
27
+
28
+    protected function configure(): void
29
+    {
30
+        $this
31
+            ->addArgument('arg1', InputArgument::OPTIONAL, 'Argument description')
32
+            ->addOption('option1', null, InputOption::VALUE_NONE, 'Option description');
33
+    }
34
+
35
+    protected function execute(InputInterface $input, OutputInterface $output): int
36
+    {
37
+        $io = new SymfonyStyle($input, $output);
38
+        $arg1 = $input->getArgument('arg1');
39
+
40
+        if ($arg1) {
41
+            $io->note(sprintf('You passed an argument: %s', $arg1));
42
+        }
43
+
44
+        if ($input->getOption('option1')) {
45
+            // ...
46
+        }
47
+
48
+        $this->mailFetcher->fetchNewEmails();
49
+
50
+
51
+        $io->success('You have a new command! Now make it your own! Pass --help to see your options.');
52
+
53
+        return Command::SUCCESS;
54
+    }
55
+}

+ 36
- 0
src/Domain/Entity/ParsedPortfolio.php ファイルの表示

@@ -0,0 +1,36 @@
1
+<?php
2
+
3
+namespace App\Domain\Entity;
4
+
5
+final readonly class ParsedPortfolio
6
+{
7
+    private array $header;
8
+
9
+    private array $details;
10
+
11
+    private array $movements;
12
+
13
+
14
+    public function __construct(array $header, array $details, array $movements)
15
+    {
16
+        $this->header = $header;
17
+        $this->details = $details;
18
+        $this->movements = $movements;
19
+    }
20
+
21
+    public function getHeader(): array
22
+    {
23
+        return $this->header;
24
+    }
25
+
26
+    public function getDetails(): array
27
+    {
28
+        return $this->details;
29
+    }
30
+
31
+    public function getMovements(): array
32
+    {
33
+        return $this->movements;
34
+    }
35
+
36
+}

+ 0
- 0
src/Entity/.gitignore ファイルの表示


+ 161
- 0
src/Entity/Portfolio.php ファイルの表示

@@ -0,0 +1,161 @@
1
+<?php
2
+
3
+namespace App\Entity;
4
+
5
+use App\Repository\PortfolioRepository;
6
+use Doctrine\Common\Collections\ArrayCollection;
7
+use Doctrine\Common\Collections\Collection;
8
+use Doctrine\DBAL\Types\Types;
9
+use Doctrine\ORM\Mapping as ORM;
10
+
11
+#[ORM\Entity(repositoryClass: PortfolioRepository::class)]
12
+class Portfolio
13
+{
14
+    #[ORM\Id]
15
+    #[ORM\GeneratedValue(strategy: "IDENTITY")]
16
+    #[ORM\Column]
17
+    private ?int $id = null;
18
+
19
+    #[ORM\Column(length: 255)]
20
+    private ?string $clientAgreement = null;
21
+
22
+    #[ORM\Column(type: Types::TEXT)]
23
+    private ?string $xmlData = null;
24
+
25
+    #[ORM\Column(type: Types::DATE_IMMUTABLE)]
26
+    private ?\DateTimeImmutable $startDate = null;
27
+
28
+    #[ORM\Column(type: Types::DATE_IMMUTABLE)]
29
+    private ?\DateTimeImmutable $endDate = null;
30
+
31
+    /**
32
+     * @var Collection<int, PortfolioDetail>
33
+     */
34
+    #[ORM\OneToMany(targetEntity: PortfolioDetail::class, mappedBy: 'portfolio')]
35
+    private Collection $portfolioDetails;
36
+
37
+    /**
38
+     * @var Collection<int, PortfolioMovement>
39
+     */
40
+    #[ORM\OneToMany(targetEntity: PortfolioMovement::class, mappedBy: 'portfolio')]
41
+    private Collection $portfolioMovements;
42
+
43
+    public function __construct()
44
+    {
45
+        $this->portfolioDetails = new ArrayCollection();
46
+        $this->portfolioMovements = new ArrayCollection();
47
+    }
48
+
49
+    public function getId(): ?int
50
+    {
51
+        return $this->id;
52
+    }
53
+
54
+    public function getClientAgreement(): ?string
55
+    {
56
+        return $this->clientAgreement;
57
+    }
58
+
59
+    public function setClientAgreement(string $clientAgreement): static
60
+    {
61
+        $this->clientAgreement = $clientAgreement;
62
+
63
+        return $this;
64
+    }
65
+
66
+    public function getXmlData(): ?string
67
+    {
68
+        return $this->xmlData;
69
+    }
70
+
71
+    public function setXmlData(string $xmlData): static
72
+    {
73
+        $this->xmlData = $xmlData;
74
+
75
+        return $this;
76
+    }
77
+
78
+    public function getStartDate(): ?\DateTimeImmutable
79
+    {
80
+        return $this->startDate;
81
+    }
82
+
83
+    public function setStartDate(\DateTimeImmutable $startDate): static
84
+    {
85
+        $this->startDate = $startDate;
86
+
87
+        return $this;
88
+    }
89
+
90
+    public function getEndDate(): ?\DateTimeImmutable
91
+    {
92
+        return $this->endDate;
93
+    }
94
+
95
+    public function setEndDate(\DateTimeImmutable $endDate): static
96
+    {
97
+        $this->endDate = $endDate;
98
+
99
+        return $this;
100
+    }
101
+
102
+    /**
103
+     * @return Collection<int, PortfolioDetail>
104
+     */
105
+    public function getPortfolioDetails(): Collection
106
+    {
107
+        return $this->portfolioDetails;
108
+    }
109
+
110
+    public function addPortfolioDetail(PortfolioDetail $portfolioDetail): static
111
+    {
112
+        if (!$this->portfolioDetails->contains($portfolioDetail)) {
113
+            $this->portfolioDetails->add($portfolioDetail);
114
+            $portfolioDetail->setPortfolio($this);
115
+        }
116
+
117
+        return $this;
118
+    }
119
+
120
+    public function removePortfolioDetail(PortfolioDetail $portfolioDetail): static
121
+    {
122
+        if ($this->portfolioDetails->removeElement($portfolioDetail)) {
123
+            // set the owning side to null (unless already changed)
124
+            if ($portfolioDetail->getPortfolio() === $this) {
125
+                $portfolioDetail->setPortfolio(null);
126
+            }
127
+        }
128
+
129
+        return $this;
130
+    }
131
+
132
+    /**
133
+     * @return Collection<int, PortfolioMovement>
134
+     */
135
+    public function getPortfolioMovements(): Collection
136
+    {
137
+        return $this->portfolioMovements;
138
+    }
139
+
140
+    public function addPortfolioMovement(PortfolioMovement $portfolioMovement): static
141
+    {
142
+        if (!$this->portfolioMovements->contains($portfolioMovement)) {
143
+            $this->portfolioMovements->add($portfolioMovement);
144
+            $portfolioMovement->setPortfolio($this);
145
+        }
146
+
147
+        return $this;
148
+    }
149
+
150
+    public function removePortfolioMovement(PortfolioMovement $portfolioMovement): static
151
+    {
152
+        if ($this->portfolioMovements->removeElement($portfolioMovement)) {
153
+            // set the owning side to null (unless already changed)
154
+            if ($portfolioMovement->getPortfolio() === $this) {
155
+                $portfolioMovement->setPortfolio(null);
156
+            }
157
+        }
158
+
159
+        return $this;
160
+    }
161
+}

+ 157
- 0
src/Entity/PortfolioDetail.php ファイルの表示

@@ -0,0 +1,157 @@
1
+<?php
2
+
3
+namespace App\Entity;
4
+
5
+use App\Repository\PortfolioDetailRepository;
6
+use Doctrine\DBAL\Types\Types;
7
+use Doctrine\ORM\Mapping as ORM;
8
+
9
+#[ORM\Entity(repositoryClass: PortfolioDetailRepository::class)]
10
+class PortfolioDetail
11
+{
12
+    #[ORM\Id]
13
+    #[ORM\GeneratedValue(strategy: "IDENTITY")]
14
+    #[ORM\Column]
15
+    private ?int $id = null;
16
+
17
+    #[ORM\Column(length: 32)]
18
+    private ?string $security = null;
19
+
20
+    #[ORM\Column]
21
+    private ?int $quantityStart = null;
22
+
23
+    #[ORM\Column]
24
+    private ?int $quantityEnd = null;
25
+
26
+    #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 3)]
27
+    private ?string $priceStart = null;
28
+
29
+    #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 3)]
30
+    private ?string $priceEnd = null;
31
+
32
+    #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 3)]
33
+    private ?string $sumStart = null;
34
+
35
+    #[ORM\Column(type: Types::DECIMAL, precision: 10, scale: 3)]
36
+    private ?string $sumEnd = null;
37
+
38
+    #[ORM\Column(length: 255)]
39
+    private ?string $issuer = null;
40
+
41
+    #[ORM\ManyToOne(inversedBy: 'portfolioDetails')]
42
+    #[ORM\JoinColumn(nullable: false)]
43
+    private ?Portfolio $portfolio = null;
44
+
45
+    public function getId(): ?int
46
+    {
47
+        return $this->id;
48
+    }
49
+
50
+    public function getSecurity(): ?string
51
+    {
52
+        return $this->security;
53
+    }
54
+
55
+    public function setSecurity(string $security): static
56
+    {
57
+        $this->security = $security;
58
+
59
+        return $this;
60
+    }
61
+
62
+    public function getQuantityStart(): ?int
63
+    {
64
+        return $this->quantityStart;
65
+    }
66
+
67
+    public function setQuantityStart(int $quantityStart): static
68
+    {
69
+        $this->quantityStart = $quantityStart;
70
+
71
+        return $this;
72
+    }
73
+
74
+    public function getQuantityEnd(): ?int
75
+    {
76
+        return $this->quantityEnd;
77
+    }
78
+
79
+    public function setQuantityEnd(int $quantityEnd): static
80
+    {
81
+        $this->quantityEnd = $quantityEnd;
82
+
83
+        return $this;
84
+    }
85
+
86
+    public function getPriceStart(): ?string
87
+    {
88
+        return $this->priceStart;
89
+    }
90
+
91
+    public function setPriceStart(string $priceStart): static
92
+    {
93
+        $this->priceStart = $priceStart;
94
+
95
+        return $this;
96
+    }
97
+
98
+    public function getPriceEnd(): ?string
99
+    {
100
+        return $this->priceEnd;
101
+    }
102
+
103
+    public function setPriceEnd(string $priceEnd): static
104
+    {
105
+        $this->priceEnd = $priceEnd;
106
+
107
+        return $this;
108
+    }
109
+
110
+    public function getSumStart(): ?string
111
+    {
112
+        return $this->sumStart;
113
+    }
114
+
115
+    public function setSumStart(string $sumStart): static
116
+    {
117
+        $this->sumStart = $sumStart;
118
+
119
+        return $this;
120
+    }
121
+
122
+    public function getSumEnd(): ?string
123
+    {
124
+        return $this->sumEnd;
125
+    }
126
+
127
+    public function setSumEnd(string $sumEnd): static
128
+    {
129
+        $this->sumEnd = $sumEnd;
130
+
131
+        return $this;
132
+    }
133
+
134
+    public function getIssuer(): ?string
135
+    {
136
+        return $this->issuer;
137
+    }
138
+
139
+    public function setIssuer(string $issuer): static
140
+    {
141
+        $this->issuer = $issuer;
142
+
143
+        return $this;
144
+    }
145
+
146
+    public function getPortfolio(): ?Portfolio
147
+    {
148
+        return $this->portfolio;
149
+    }
150
+
151
+    public function setPortfolio(?Portfolio $portfolio): static
152
+    {
153
+        $this->portfolio = $portfolio;
154
+
155
+        return $this;
156
+    }
157
+}

+ 126
- 0
src/Entity/PortfolioMovement.php ファイルの表示

@@ -0,0 +1,126 @@
1
+<?php
2
+
3
+namespace App\Entity;
4
+
5
+use App\Repository\PortfolioMovementRepository;
6
+use Doctrine\ORM\Mapping as ORM;
7
+
8
+#[ORM\Entity(repositoryClass: PortfolioMovementRepository::class)]
9
+class PortfolioMovement
10
+{
11
+    #[ORM\Id]
12
+    #[ORM\GeneratedValue(strategy: "IDENTITY")]
13
+    #[ORM\Column]
14
+    private ?int $id = null;
15
+
16
+    #[ORM\Column(length: 255)]
17
+    private ?string $security = null;
18
+
19
+    #[ORM\Column]
20
+    private ?\DateTimeImmutable $period = null;
21
+
22
+    #[ORM\Column]
23
+    private ?int $quantityStart = null;
24
+
25
+    #[ORM\Column]
26
+    private ?int $quantityEnd = null;
27
+
28
+    #[ORM\Column]
29
+    private ?int $quantityIncome = null;
30
+
31
+    #[ORM\Column]
32
+    private ?int $quantityOutcome = null;
33
+
34
+    #[ORM\ManyToOne(inversedBy: 'portfolioMovements')]
35
+    #[ORM\JoinColumn(nullable: false)]
36
+    private ?Portfolio $portfolio = null;
37
+
38
+    public function getId(): ?int
39
+    {
40
+        return $this->id;
41
+    }
42
+
43
+    public function getSecurity(): ?string
44
+    {
45
+        return $this->security;
46
+    }
47
+
48
+    public function setSecurity(string $security): static
49
+    {
50
+        $this->security = $security;
51
+
52
+        return $this;
53
+    }
54
+
55
+    public function getPeriod(): ?\DateTimeImmutable
56
+    {
57
+        return $this->period;
58
+    }
59
+
60
+    public function setPeriod(\DateTimeImmutable $period): static
61
+    {
62
+        $this->period = $period;
63
+
64
+        return $this;
65
+    }
66
+
67
+    public function getQuantityStart(): ?int
68
+    {
69
+        return $this->quantityStart;
70
+    }
71
+
72
+    public function setQuantityStart(int $quantityStart): static
73
+    {
74
+        $this->quantityStart = $quantityStart;
75
+
76
+        return $this;
77
+    }
78
+
79
+    public function getQuantityEnd(): ?int
80
+    {
81
+        return $this->quantityEnd;
82
+    }
83
+
84
+    public function setQuantityEnd(int $quantityEnd): static
85
+    {
86
+        $this->quantityEnd = $quantityEnd;
87
+
88
+        return $this;
89
+    }
90
+
91
+    public function getQuantityIncome(): ?int
92
+    {
93
+        return $this->quantityIncome;
94
+    }
95
+
96
+    public function setQuantityIncome(int $quantityIncome): static
97
+    {
98
+        $this->quantityIncome = $quantityIncome;
99
+
100
+        return $this;
101
+    }
102
+
103
+    public function getQuantityOutcome(): ?int
104
+    {
105
+        return $this->quantityOutcome;
106
+    }
107
+
108
+    public function setQuantityOutcome(int $quantityOutcome): static
109
+    {
110
+        $this->quantityOutcome = $quantityOutcome;
111
+
112
+        return $this;
113
+    }
114
+
115
+    public function getPortfolio(): ?Portfolio
116
+    {
117
+        return $this->portfolio;
118
+    }
119
+
120
+    public function setPortfolio(?Portfolio $portfolio): static
121
+    {
122
+        $this->portfolio = $portfolio;
123
+
124
+        return $this;
125
+    }
126
+}

+ 69
- 0
src/Presentation/PortfolioPresenter.php ファイルの表示

@@ -0,0 +1,69 @@
1
+<?php
2
+
3
+namespace App\Presentation;
4
+
5
+use App\Domain\Entity\ParsedPortfolio;
6
+use Symfony\Component\Console\Helper\Table;
7
+use Symfony\Component\Console\Output\BufferedOutput;
8
+use Symfony\Component\Console\Helper\TableStyle;
9
+
10
+class PortfolioPresenter
11
+{
12
+    public static function toPrint(ParsedPortfolio $parsedPortfolio): string
13
+    {
14
+        $output = new BufferedOutput();
15
+
16
+        $tableStyle = new TableStyle();
17
+        $tableStyle->setPadType(STR_PAD_BOTH);
18
+
19
+        $table = new Table($output);
20
+
21
+        $table->setHeaderTitle("СОСТАВ: {$parsedPortfolio->getHeader()['НачПериода']} - {$parsedPortfolio->getHeader()['КонПериода']}");
22
+
23
+        $table->setHeaders([
24
+            'ЦБ', 'НКол.', 'НЦена', 'ККол.', 'КЦена'
25
+        ]);
26
+
27
+        foreach ($parsedPortfolio->getDetails() as $detail) {
28
+            $table->addRow([
29
+                $detail['ЦБ'],// . ' ' . $detail['Эмитент'],
30
+                $detail['КоличествоНО'],
31
+                $detail['НОЦена'],
32
+                //$detail['СуммаНКДНО'],
33
+                $detail['КоличествоКО'],
34
+                $detail['КОЦена'],
35
+                //$detail['СуммаНКДКО'],
36
+            ]);
37
+        }
38
+
39
+        $table->setStyle($tableStyle)->render();
40
+
41
+        $txt = $output->fetch();
42
+
43
+        $table = new Table($output);
44
+
45
+
46
+        $table->setHeaderTitle("ДВИЖ: {$parsedPortfolio->getHeader()['НачПериода']} - {$parsedPortfolio->getHeader()['КонПериода']}");
47
+
48
+        $table->setHeaders([
49
+            'ЦБ', 'НКол.', 'Приход', 'Расход', 'ККол.'
50
+        ]);
51
+
52
+        foreach ($parsedPortfolio->getMovements() as $movement) {
53
+            $table->addRow([
54
+                $movement['ЦБ'],
55
+                //$movement['Период'],
56
+                $movement['НО'],
57
+                $movement['Приход']>0 ? '+'.$movement['Приход'] : $movement['Приход'],
58
+                $movement['Расход']>0 ? '-'.$movement['Расход'] : $movement['Расход'],
59
+                $movement['КО'],
60
+            ]);
61
+        }
62
+
63
+        $table->setStyle($tableStyle)->render();
64
+        $txt .= PHP_EOL . $output->fetch();
65
+
66
+        return $txt;
67
+    }
68
+
69
+}

+ 0
- 0
src/Repository/.gitignore ファイルの表示


+ 43
- 0
src/Repository/PortfolioDetailRepository.php ファイルの表示

@@ -0,0 +1,43 @@
1
+<?php
2
+
3
+namespace App\Repository;
4
+
5
+use App\Entity\PortfolioDetail;
6
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
7
+use Doctrine\Persistence\ManagerRegistry;
8
+
9
+/**
10
+ * @extends ServiceEntityRepository<PortfolioDetail>
11
+ */
12
+class PortfolioDetailRepository extends ServiceEntityRepository
13
+{
14
+    public function __construct(ManagerRegistry $registry)
15
+    {
16
+        parent::__construct($registry, PortfolioDetail::class);
17
+    }
18
+
19
+    //    /**
20
+    //     * @return PortfolioDetail[] Returns an array of PortfolioDetail objects
21
+    //     */
22
+    //    public function findByExampleField($value): array
23
+    //    {
24
+    //        return $this->createQueryBuilder('p')
25
+    //            ->andWhere('p.exampleField = :val')
26
+    //            ->setParameter('val', $value)
27
+    //            ->orderBy('p.id', 'ASC')
28
+    //            ->setMaxResults(10)
29
+    //            ->getQuery()
30
+    //            ->getResult()
31
+    //        ;
32
+    //    }
33
+
34
+    //    public function findOneBySomeField($value): ?PortfolioDetail
35
+    //    {
36
+    //        return $this->createQueryBuilder('p')
37
+    //            ->andWhere('p.exampleField = :val')
38
+    //            ->setParameter('val', $value)
39
+    //            ->getQuery()
40
+    //            ->getOneOrNullResult()
41
+    //        ;
42
+    //    }
43
+}

+ 43
- 0
src/Repository/PortfolioMovementRepository.php ファイルの表示

@@ -0,0 +1,43 @@
1
+<?php
2
+
3
+namespace App\Repository;
4
+
5
+use App\Entity\PortfolioMovement;
6
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
7
+use Doctrine\Persistence\ManagerRegistry;
8
+
9
+/**
10
+ * @extends ServiceEntityRepository<PortfolioMovement>
11
+ */
12
+class PortfolioMovementRepository extends ServiceEntityRepository
13
+{
14
+    public function __construct(ManagerRegistry $registry)
15
+    {
16
+        parent::__construct($registry, PortfolioMovement::class);
17
+    }
18
+
19
+    //    /**
20
+    //     * @return PortfolioMovement[] Returns an array of PortfolioMovement objects
21
+    //     */
22
+    //    public function findByExampleField($value): array
23
+    //    {
24
+    //        return $this->createQueryBuilder('p')
25
+    //            ->andWhere('p.exampleField = :val')
26
+    //            ->setParameter('val', $value)
27
+    //            ->orderBy('p.id', 'ASC')
28
+    //            ->setMaxResults(10)
29
+    //            ->getQuery()
30
+    //            ->getResult()
31
+    //        ;
32
+    //    }
33
+
34
+    //    public function findOneBySomeField($value): ?PortfolioMovement
35
+    //    {
36
+    //        return $this->createQueryBuilder('p')
37
+    //            ->andWhere('p.exampleField = :val')
38
+    //            ->setParameter('val', $value)
39
+    //            ->getQuery()
40
+    //            ->getOneOrNullResult()
41
+    //        ;
42
+    //    }
43
+}

+ 43
- 0
src/Repository/PortfolioRepository.php ファイルの表示

@@ -0,0 +1,43 @@
1
+<?php
2
+
3
+namespace App\Repository;
4
+
5
+use App\Entity\Portfolio;
6
+use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
7
+use Doctrine\Persistence\ManagerRegistry;
8
+
9
+/**
10
+ * @extends ServiceEntityRepository<Portfolio>
11
+ */
12
+class PortfolioRepository extends ServiceEntityRepository
13
+{
14
+    public function __construct(ManagerRegistry $registry)
15
+    {
16
+        parent::__construct($registry, Portfolio::class);
17
+    }
18
+
19
+//    /**
20
+//     * @return ReportHeader[] Returns an array of ReportHeader objects
21
+//     */
22
+//    public function findByExampleField($value): array
23
+//    {
24
+//        return $this->createQueryBuilder('r')
25
+//            ->andWhere('r.exampleField = :val')
26
+//            ->setParameter('val', $value)
27
+//            ->orderBy('r.id', 'ASC')
28
+//            ->setMaxResults(10)
29
+//            ->getQuery()
30
+//            ->getResult()
31
+//        ;
32
+//    }
33
+
34
+//    public function findOneBySomeField($value): ?ReportHeader
35
+//    {
36
+//        return $this->createQueryBuilder('r')
37
+//            ->andWhere('r.exampleField = :val')
38
+//            ->setParameter('val', $value)
39
+//            ->getQuery()
40
+//            ->getOneOrNullResult()
41
+//        ;
42
+//    }
43
+}

+ 167
- 0
src/Service/MailFetcher.php ファイルの表示

@@ -0,0 +1,167 @@
1
+<?php
2
+
3
+namespace App\Service;
4
+
5
+use PhpImap\IncomingMail;
6
+use PhpImap\IncomingMailAttachment;
7
+use Psr\Log\LoggerInterface;
8
+use SecIT\ImapBundle\Connection\ConnectionInterface;
9
+use Symfony\Component\DependencyInjection\Attribute\Target;
10
+use Symfony\Component\Filesystem\Filesystem;
11
+use Symfony\Component\Filesystem\Path;
12
+use App\Presentation\PortfolioPresenter as PresentationPortfolio;
13
+use Symfony\Component\Notifier\Bridge\Telegram\TelegramOptions;
14
+use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransport;
15
+use Symfony\Component\Notifier\Chatter;
16
+use Symfony\Component\Notifier\Message\ChatMessage;
17
+use Symfony\Component\Notifier\Notifier;
18
+use Symfony\Component\Notifier\NotifierInterface;
19
+use App\Service\TelegramNotifier;
20
+
21
+readonly class MailFetcher
22
+{
23
+    public function __construct(
24
+        #[Target('finfollowConnection')]
25
+        private ConnectionInterface   $connection,
26
+        private XmlParser             $xmlParser,
27
+        private PortfolioManager      $portfolioManager,
28
+        private LoggerInterface       $logger,
29
+        private TelegramNotifier      $telegramNotifier,
30
+    )
31
+    {
32
+    }
33
+
34
+    public function fetchNewEmails(): void
35
+    {
36
+        $mailbox = $this->connection->getMailbox();
37
+        try {
38
+            // Get all emails (messages)
39
+            // PHP.net imap_search criteria: http://php.net/manual/en/function.imap-search.php
40
+            $mailsIds = $mailbox->searchMailbox('ALL');
41
+        } catch (PhpImap\Exceptions\ConnectionException $ex) {
42
+            $this->logger->error("IMAP connection failed: " . implode(",", $ex->getErrors('all')));
43
+            die();
44
+        }
45
+
46
+        if (!$mailsIds) {
47
+            $this->logger->info('Mailbox is empty');
48
+
49
+            $this->logger->warn("Here we should send info about no changes performed..");
50
+
51
+            return;
52
+        }
53
+
54
+// If '__DIR__' was defined in the first line, it will automatically
55
+// save all attachments to the specified directory
56
+
57
+        foreach ($mailsIds as $mailId) {
58
+            $mail = $mailbox->getMail($mailId);
59
+
60
+            $this->logger->debug(print_r([
61
+                'date' => $mail->date,
62
+                'message_id' => $mail->messageId,
63
+                'from' => $mail->fromAddress,
64
+                'subject' => $mail->subject,
65
+                'hasAttachments' => $mail->hasAttachments(),
66
+            ], true));
67
+
68
+            if ($this->canProcessMail($mail)) {
69
+                if ($this->processMail($mail)) {
70
+
71
+                    //break;
72
+
73
+                }
74
+
75
+                //$mailbox->deleteMail($mailId);
76
+                $this->logger->debug("Deleted email with id=" . $mailId);
77
+            } else {
78
+                $this->logger->debug("Skipped mail with id=" . $mailId);
79
+            }
80
+        }
81
+
82
+        $mailbox->disconnect();
83
+    }
84
+
85
+    private function canProcessMail(IncomingMail $mail): bool
86
+    {
87
+        if (!$mail->hasAttachments()) {
88
+            return false;
89
+        }
90
+
91
+        if (mb_stripos($mail->subject, 'Broker report') === false) {
92
+            return false;
93
+        }
94
+
95
+        if (mb_stripos($mail->fromAddress, '@bcs.ru') === false) {
96
+            return false;
97
+        }
98
+
99
+        return true;
100
+    }
101
+
102
+    private function processMail(IncomingMail $mail): bool
103
+    {
104
+        $mailProcessed = false;
105
+
106
+        foreach ($mail->getAttachments() as $attachment) {
107
+            if ($attachment->mimeType != 'application/zip') {
108
+                continue;
109
+            }
110
+
111
+            $mailProcessed |= $this->processZipArchiveAttachment($attachment);
112
+        }
113
+
114
+        return $mailProcessed;
115
+    }
116
+
117
+    private function processZipArchiveAttachment(IncomingMailAttachment $attachment): bool
118
+    {
119
+        $zip = new \ZipArchive();
120
+        if ($zip->open($attachment->filePath) !== true) {
121
+            $this->logger->error('ZIP open error: ' . $zip);
122
+            return false;
123
+        }
124
+
125
+        $xmlFound = false;
126
+        for ($i = 0; $i < $zip->numFiles; $i++) {
127
+            $stat = $zip->statIndex($i);
128
+
129
+            if (pathinfo($stat['name'])['extension'] !== 'xml') {
130
+                continue;
131
+            }
132
+
133
+            $tmpdir = $this->getTmpDir();
134
+
135
+            $zip->extractTo($tmpdir, [$stat['name']]);
136
+            $this->logger->debug("Extracted '$stat[name]' into '$tmpdir'");
137
+
138
+            $xmlString = file_get_contents($tmpdir . '/' . $stat['name']);
139
+            $xml = simplexml_load_string($xmlString);
140
+
141
+            $parsedPortfolio = $this->xmlParser->processXml($xml);
142
+            $message = PresentationPortfolio::toPrint($parsedPortfolio);
143
+
144
+             if ($this->portfolioManager->updatePortfolio($parsedPortfolio, $xmlString)) {
145
+                 $this->telegramNotifier->notify($message);
146
+            }
147
+
148
+            $fs = new Filesystem();
149
+            $fs->remove($tmpdir);
150
+
151
+            $xmlFound = true;
152
+        }
153
+
154
+        return $xmlFound;
155
+    }
156
+
157
+    private function getTmpDir(): string
158
+    {
159
+        $tmpdir = Path::normalize(sys_get_temp_dir() . '/FINFOLLOW-' . random_int(0, 100));
160
+
161
+        $filesystem = new Filesystem();
162
+        $filesystem->mkdir($tmpdir);
163
+
164
+        return $tmpdir;
165
+    }
166
+
167
+}

+ 89
- 0
src/Service/PortfolioManager.php ファイルの表示

@@ -0,0 +1,89 @@
1
+<?php
2
+
3
+namespace App\Service;
4
+
5
+use App\Domain\Entity\ParsedPortfolio;
6
+use App\Entity\Portfolio;
7
+use App\Entity\PortfolioDetail;
8
+use App\Entity\PortfolioMovement;
9
+use Doctrine\ORM\EntityManagerInterface;
10
+use Psr\Log\LoggerInterface;
11
+
12
+readonly class PortfolioManager
13
+{
14
+    public function __construct(private EntityManagerInterface $entityManager, private LoggerInterface $logger)
15
+    {
16
+    }
17
+
18
+    public function updatePortfolio(ParsedPortfolio $parsedPortfolio, string $xmlString): bool
19
+    {
20
+        $startDate = \DateTime::createFromFormat('d.m.Y', $parsedPortfolio->getHeader()['НачПериода']);
21
+        $endDate = \DateTime::createFromFormat('d.m.Y', $parsedPortfolio->getHeader()['КонПериода']);
22
+
23
+        $portfolio = $this->entityManager->getRepository(Portfolio::class)->findOneBy([
24
+            'clientAgreement' => $parsedPortfolio->getHeader()['Клиент'],
25
+            'startDate' => new \DateTimeImmutable($startDate->format('Y-m-d')),
26
+            'endDate' => new \DateTimeImmutable($endDate->format('Y-m-d')),
27
+        ]);
28
+
29
+        if ($portfolio) {
30
+            $this->logger->debug("Found existing portfolio with id: " . $portfolio->getId() . ', skipped..');
31
+            return false;
32
+        } else {
33
+            $newPortfolio = $this->savePortfolio($parsedPortfolio, $xmlString);
34
+            $this->logger->debug("Saved new portfolio with id: " . $newPortfolio->getId());
35
+            return true;
36
+        }
37
+    }
38
+
39
+    private function savePortfolio(ParsedPortfolio $parsedPortfolio, string $xmlString): Portfolio
40
+    {
41
+        $this->logger->debug(print_r($parsedPortfolio, true));
42
+
43
+        $startDate = \DateTime::createFromFormat('d.m.Y', $parsedPortfolio->getHeader()['НачПериода']);
44
+        $endDate = \DateTime::createFromFormat('d.m.Y', $parsedPortfolio->getHeader()['КонПериода']);
45
+
46
+        $portfolio = new Portfolio();
47
+
48
+        $portfolio->setClientAgreement($parsedPortfolio->getHeader()['Клиент']);
49
+        $portfolio->setStartDate(new \DateTimeImmutable($startDate->format('Y-m-d')));
50
+        $portfolio->setEndDate(new \DateTimeImmutable($endDate->format('Y-m-d')));
51
+        $portfolio->setXmlData($xmlString);
52
+
53
+        $this->entityManager->persist($portfolio);
54
+
55
+        foreach ($parsedPortfolio->getDetails() as $parsedDetail) {
56
+            $detail = new PortfolioDetail();
57
+
58
+            $detail->setPortfolio($portfolio);
59
+            $detail->setIssuer($parsedDetail['Эмитент']);
60
+            $detail->setSecurity($parsedDetail['ЦБ']);
61
+            $detail->setPriceStart($parsedDetail['НОЦена']);
62
+            $detail->setPriceEnd($parsedDetail['КОЦена']);
63
+            $detail->setQuantityStart($parsedDetail['КоличествоНО']);
64
+            $detail->setQuantityEnd($parsedDetail['КоличествоКО']);
65
+            $detail->setSumStart($parsedDetail['СуммаНКДНО']);
66
+            $detail->setSumEnd($parsedDetail['СуммаНКДКО']);
67
+
68
+            $this->entityManager->persist($detail);
69
+        }
70
+
71
+        foreach ($parsedPortfolio->getMovements() as $parsedMovement) {
72
+            $movement = new PortfolioMovement();
73
+
74
+            $movement->setPortfolio($portfolio);
75
+            $movement->setSecurity($parsedMovement['ЦБ']);
76
+            $movement->setPeriod(new \DateTimeImmutable($parsedMovement['Период']));
77
+            $movement->setQuantityStart($parsedMovement['НО']);
78
+            $movement->setQuantityEnd($parsedMovement['КО']);
79
+            $movement->setQuantityIncome($parsedMovement['Приход']);
80
+            $movement->setQuantityOutcome($parsedMovement['Расход']);
81
+
82
+            $this->entityManager->persist($movement);
83
+        }
84
+
85
+        $this->entityManager->flush();
86
+
87
+        return $portfolio;
88
+    }
89
+}

+ 33
- 0
src/Service/TelegramNotifier.php ファイルの表示

@@ -0,0 +1,33 @@
1
+<?php
2
+
3
+namespace App\Service;
4
+
5
+use Symfony\Component\Notifier\Bridge\Telegram\TelegramTransport;
6
+use Symfony\Component\Notifier\Chatter;
7
+use Symfony\Component\Notifier\Bridge\Telegram\TelegramOptions;
8
+use Symfony\Component\Notifier\Message\ChatMessage;
9
+
10
+readonly class TelegramNotifier
11
+{
12
+    private string $telegramBotToken;
13
+    private string $telegramChatId;
14
+
15
+    public function __construct(string $telegramBotToken, string $telegramChatId)
16
+    {
17
+        $this->telegramBotToken = $telegramBotToken;
18
+        $this->telegramChatId = $telegramChatId;
19
+    }
20
+
21
+    public function notify(string $message): void
22
+    {
23
+        $telegramTransport = new TelegramTransport($this->telegramBotToken, $this->telegramChatId);
24
+        $chatter = new Chatter($telegramTransport);
25
+        $chatMessage = new ChatMessage('<pre>' . $message . '</pre>');
26
+
27
+        $telegramOptions = new TelegramOptions();
28
+        $telegramOptions->parseMode(TelegramOptions::PARSE_MODE_HTML);
29
+        $chatMessage->options($telegramOptions);
30
+
31
+        $chatter->send($chatMessage);
32
+    }
33
+}

+ 59
- 0
src/Service/XmlParser.php ファイルの表示

@@ -0,0 +1,59 @@
1
+<?php
2
+
3
+namespace App\Service;
4
+
5
+use App\Domain\Entity\ParsedPortfolio;
6
+
7
+class XmlParser
8
+{
9
+    public function processXml(\SimpleXMLElement $xml): ParsedPortfolio
10
+    {
11
+        $portfolio = [
12
+            'header' => $this->processXmlHeader($xml),
13
+            'details' => $this->processXmlElement($xml->{'ЗаголовокПортфель'}->{'ШапкаПортфель'}),
14
+            'movements' => $this->processXmlElement($xml->{'ШапкаДвиженияИОстаткиЦБ'}),
15
+        ];
16
+
17
+        return new ParsedPortfolio(...$portfolio);
18
+    }
19
+
20
+
21
+    private function processXmlHeader(\SimpleXMLElement $xml): array
22
+    {
23
+        $header = [];
24
+        foreach (['ШапкаГенСог', 'ШапкаПериод'] as $h) {
25
+            foreach ($xml->{$h}->attributes() as $key => $value) {
26
+                $header[$key] = get_object_vars($value)[0];
27
+            }
28
+        }
29
+
30
+        return $header;
31
+    }
32
+
33
+    private function processXmlElement(\SimpleXMLElement $xml): array
34
+    {
35
+        $securities = [];
36
+
37
+        foreach (get_object_vars($xml) as $key => $value) {
38
+
39
+            if (mb_strpos($key, 'Детали') !== false) {
40
+
41
+                if (!is_array($value)) {
42
+                    $value = [$value];
43
+                }
44
+
45
+                foreach ($value as $line) {
46
+                    $attr = [];
47
+
48
+                    foreach ($line->attributes() as $k1 => $v1) {
49
+                        $attr[$k1] = get_object_vars($v1)[0];
50
+                    }
51
+
52
+                    $securities[$attr['ЦБ']] = $attr;
53
+                }
54
+            }
55
+        }
56
+
57
+        return $securities;
58
+    }
59
+}

+ 81
- 0
symfony.lock ファイルの表示

@@ -1,4 +1,43 @@
1 1
 {
2
+    "doctrine/doctrine-bundle": {
3
+        "version": "2.12",
4
+        "recipe": {
5
+            "repo": "github.com/symfony/recipes",
6
+            "branch": "main",
7
+            "version": "2.12",
8
+            "ref": "7266981c201efbbe02ae53c87f8bb378e3f825ae"
9
+        },
10
+        "files": [
11
+            "config/packages/doctrine.yaml",
12
+            "src/Entity/.gitignore",
13
+            "src/Repository/.gitignore"
14
+        ]
15
+    },
16
+    "doctrine/doctrine-migrations-bundle": {
17
+        "version": "3.3",
18
+        "recipe": {
19
+            "repo": "github.com/symfony/recipes",
20
+            "branch": "main",
21
+            "version": "3.1",
22
+            "ref": "1d01ec03c6ecbd67c3375c5478c9a423ae5d6a33"
23
+        },
24
+        "files": [
25
+            "config/packages/doctrine_migrations.yaml",
26
+            "migrations/.gitignore"
27
+        ]
28
+    },
29
+    "secit-pl/imap-bundle": {
30
+        "version": "3.2",
31
+        "recipe": {
32
+            "repo": "github.com/symfony/recipes-contrib",
33
+            "branch": "main",
34
+            "version": "1.0",
35
+            "ref": "2303e00839856f36b1829d0b189e5d959153aefb"
36
+        },
37
+        "files": [
38
+            "config/packages/imap.yaml"
39
+        ]
40
+    },
2 41
     "symfony/console": {
3 42
         "version": "7.1",
4 43
         "recipe": {
@@ -42,6 +81,39 @@
42 81
             "src/Kernel.php"
43 82
         ]
44 83
     },
84
+    "symfony/maker-bundle": {
85
+        "version": "1.59",
86
+        "recipe": {
87
+            "repo": "github.com/symfony/recipes",
88
+            "branch": "main",
89
+            "version": "1.0",
90
+            "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
91
+        }
92
+    },
93
+    "symfony/monolog-bundle": {
94
+        "version": "3.10",
95
+        "recipe": {
96
+            "repo": "github.com/symfony/recipes",
97
+            "branch": "main",
98
+            "version": "3.7",
99
+            "ref": "aff23899c4440dd995907613c1dd709b6f59503f"
100
+        },
101
+        "files": [
102
+            "config/packages/monolog.yaml"
103
+        ]
104
+    },
105
+    "symfony/notifier": {
106
+        "version": "7.1",
107
+        "recipe": {
108
+            "repo": "github.com/symfony/recipes",
109
+            "branch": "main",
110
+            "version": "5.0",
111
+            "ref": "178877daf79d2dbd62129dd03612cb1a2cb407cc"
112
+        },
113
+        "files": [
114
+            "config/packages/notifier.yaml"
115
+        ]
116
+    },
45 117
     "symfony/routing": {
46 118
         "version": "7.1",
47 119
         "recipe": {
@@ -54,5 +126,14 @@
54 126
             "config/packages/routing.yaml",
55 127
             "config/routes.yaml"
56 128
         ]
129
+    },
130
+    "symfony/telegram-notifier": {
131
+        "version": "7.1",
132
+        "recipe": {
133
+            "repo": "github.com/symfony/recipes",
134
+            "branch": "main",
135
+            "version": "5.0",
136
+            "ref": "6cecb59a0e96c9e1cee469f2b82fa920101a68e8"
137
+        }
57 138
     }
58 139
 }

読み込み中…
キャンセル
保存