feat(118): fakturownia single instance
Phase 118 complete: - migration 20260512_000109 adds single global Fakturownia settings row - FakturowniaIntegrationRepository simplified to one-instance API - FakturowniaIntegrationController + edit view collapsed to one settings page - Integrations hub shows Fakturownia as single instance - Invoice config delegated flow always uses global integration_id Note: shared routes/web.php and DOCS/* updates from Phase 118 are bundled into the follow-up feat(121+122) commit because Phase 121/122 modified the same files; hunk-level split was not performed. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
210
.paul/phases/118-fakturownia-single-instance/118-01-PLAN.md
Normal file
210
.paul/phases/118-fakturownia-single-instance/118-01-PLAN.md
Normal file
@@ -0,0 +1,210 @@
|
|||||||
|
---
|
||||||
|
phase: 118-fakturownia-single-instance
|
||||||
|
plan: 01
|
||||||
|
type: execute
|
||||||
|
wave: 1
|
||||||
|
depends_on: []
|
||||||
|
files_modified:
|
||||||
|
- database/migrations/20260512_000109_fakturownia_single_instance.sql
|
||||||
|
- src/Modules/Settings/FakturowniaIntegrationRepository.php
|
||||||
|
- src/Modules/Settings/FakturowniaIntegrationController.php
|
||||||
|
- src/Modules/Settings/IntegrationsHubController.php
|
||||||
|
- src/Modules/Settings/InvoiceConfigRepository.php
|
||||||
|
- src/Modules/Settings/InvoiceConfigController.php
|
||||||
|
- src/Modules/Accounting/InvoiceService.php
|
||||||
|
- resources/views/settings/fakturownia.php
|
||||||
|
- resources/views/settings/fakturownia-edit.php
|
||||||
|
- resources/views/settings/accounting-invoice-edit.php
|
||||||
|
- routes/web.php
|
||||||
|
- DOCS/DB_SCHEMA.md
|
||||||
|
- DOCS/ARCHITECTURE.md
|
||||||
|
- DOCS/TECH_CHANGELOG.md
|
||||||
|
autonomous: true
|
||||||
|
delegation: off
|
||||||
|
---
|
||||||
|
|
||||||
|
<objective>
|
||||||
|
## Goal
|
||||||
|
Convert Fakturownia from a multi-account integration to one global integration instance, like HostedSMS/SMSPLANET.
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
The operator should configure Fakturownia once. Invoice configs may still delegate invoice issuing to Fakturownia, but they must all point to the single global Fakturownia integration row.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
One migration, one single-instance repository/controller/UI flow, updated invoice config handling, and updated technical documentation.
|
||||||
|
</objective>
|
||||||
|
|
||||||
|
<context>
|
||||||
|
<clarifications>
|
||||||
|
- **Migracja** - Co zrobic z istniejacymi wieloma kontami Fakturowni, jesli sa juz w bazie?
|
||||||
|
-> Odpowiedz: Wybrac aktywne konto.
|
||||||
|
- **Kontrakty** - Czy delegowane konfiguracje faktur maja nadal trzymac `integration_id`, ale zawsze wskazywac jedyna globalna Fakturownie?
|
||||||
|
-> Odpowiedz: Tak.
|
||||||
|
- **UI** - Jak ma wygladac UI Fakturowni?
|
||||||
|
-> Odpowiedz: Jedna strona z formularzem konfiguracji i testem polaczenia, bez dodawania wielu instancji.
|
||||||
|
</clarifications>
|
||||||
|
|
||||||
|
## Project Context
|
||||||
|
@.paul/PROJECT.md
|
||||||
|
@.paul/ROADMAP.md
|
||||||
|
@.paul/STATE.md
|
||||||
|
@DOCS/DB_SCHEMA.md
|
||||||
|
@DOCS/ARCHITECTURE.md
|
||||||
|
|
||||||
|
## Source Files
|
||||||
|
@src/Modules/Settings/FakturowniaIntegrationRepository.php
|
||||||
|
@src/Modules/Settings/FakturowniaIntegrationController.php
|
||||||
|
@src/Modules/Settings/HostedSmsIntegrationRepository.php
|
||||||
|
@src/Modules/Settings/IntegrationsHubController.php
|
||||||
|
@src/Modules/Settings/InvoiceConfigRepository.php
|
||||||
|
@src/Modules/Settings/InvoiceConfigController.php
|
||||||
|
@src/Modules/Accounting/InvoiceService.php
|
||||||
|
@resources/views/settings/fakturownia.php
|
||||||
|
@resources/views/settings/fakturownia-edit.php
|
||||||
|
@resources/views/settings/accounting-invoice-edit.php
|
||||||
|
@routes/web.php
|
||||||
|
|
||||||
|
## Prior Work
|
||||||
|
@.paul/phases/113-fakturownia-integration/113-01-SUMMARY.md
|
||||||
|
@.paul/phases/114-accounting-configs-refactor/114-01-SUMMARY.md
|
||||||
|
@.paul/phases/115-invoice-from-order/115-01-SUMMARY.md
|
||||||
|
@.paul/phases/116-hostedsms-integration/116-01-SUMMARY.md
|
||||||
|
</context>
|
||||||
|
|
||||||
|
<skills>
|
||||||
|
## Required Skills
|
||||||
|
|
||||||
|
No blocking skills before APPLY.
|
||||||
|
|
||||||
|
Project SPECIAL-FLOWS requires `sonar-scanner` after APPLY and before UNIFY when available in PATH.
|
||||||
|
</skills>
|
||||||
|
|
||||||
|
<acceptance_criteria>
|
||||||
|
|
||||||
|
## AC-1: Jedna globalna konfiguracja Fakturowni
|
||||||
|
```gherkin
|
||||||
|
Given the operator opens /settings/integrations/fakturownia
|
||||||
|
When the page renders
|
||||||
|
Then there is one settings form with save and test actions, and no UI for adding, listing, or deleting multiple Fakturownia accounts
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-2: Migracja wybiera aktywna instancje
|
||||||
|
```gherkin
|
||||||
|
Given multiple integrations with type "fakturownia" exist before migration
|
||||||
|
When the migration runs
|
||||||
|
Then the active integration is preserved as the single global row, invoice_configs.integration_id values are reassigned to it, settings are preserved for that row, and extra unused Fakturownia rows are removed or detached safely
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-3: Delegacja faktur pozostaje kompatybilna
|
||||||
|
```gherkin
|
||||||
|
Given an invoice config has is_delegated = 1
|
||||||
|
When it is saved or used to issue an invoice
|
||||||
|
Then it points to the single global Fakturownia integration_id and InvoiceService can still call Fakturownia without schema changes
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-4: Hub integracji pokazuje pojedynczy status
|
||||||
|
```gherkin
|
||||||
|
Given the integrations hub is opened
|
||||||
|
When Fakturownia row is displayed
|
||||||
|
Then it shows one provider row with configured/active/token/test status for the global instance, not a count of instances
|
||||||
|
```
|
||||||
|
|
||||||
|
## AC-5: Dokumentacja jest aktualna
|
||||||
|
```gherkin
|
||||||
|
Given the implementation is complete
|
||||||
|
When DOCS are reviewed
|
||||||
|
Then DB_SCHEMA, ARCHITECTURE, and TECH_CHANGELOG describe Fakturownia as a fixed single-instance integration and note the migration behavior
|
||||||
|
```
|
||||||
|
|
||||||
|
</acceptance_criteria>
|
||||||
|
|
||||||
|
<tasks>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 1: Add single-instance migration</name>
|
||||||
|
<files>database/migrations/20260512_000109_fakturownia_single_instance.sql, DOCS/DB_SCHEMA.md</files>
|
||||||
|
<action>
|
||||||
|
Create an idempotent migration that enforces the single-instance Fakturownia model without dropping existing invoice history.
|
||||||
|
- Pick the preserved row by priority: active Fakturownia integration first, then the one referenced by invoice_configs, then the lowest id.
|
||||||
|
- Ensure a base integrations row exists with type=fakturownia and name=Fakturownia if none exists.
|
||||||
|
- Ensure one fakturownia_integration_settings row exists for the preserved integration.
|
||||||
|
- Reassign invoice_configs.integration_id for delegated configs to the preserved integration.
|
||||||
|
- Remove extra fakturownia_integration_settings/integrations rows only after reassignment, using prepared migration SQL and FK-safe order.
|
||||||
|
- Add/adjust constraints so settings are fixed to id=1 when feasible without breaking MySQL compatibility.
|
||||||
|
Avoid: deleting invoices, changing invoices.config_id, or removing the invoice_configs.integration_id column.
|
||||||
|
</action>
|
||||||
|
<verify>C:\xampp\php\php.exe bin\migrate.php on a database with zero, one, and multiple Fakturownia rows; inspect integrations/fakturownia_integration_settings/invoice_configs after migration.</verify>
|
||||||
|
<done>AC-2 and AC-3 satisfied.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 2: Refactor Fakturownia repository/controller/UI to single settings page</name>
|
||||||
|
<files>src/Modules/Settings/FakturowniaIntegrationRepository.php, src/Modules/Settings/FakturowniaIntegrationController.php, resources/views/settings/fakturownia.php, resources/views/settings/fakturownia-edit.php, routes/web.php, src/Modules/Settings/IntegrationsHubController.php</files>
|
||||||
|
<action>
|
||||||
|
Replace the multi-account behavior with HostedSMS-style single settings behavior.
|
||||||
|
- Repository exposes getSettings(), saveSettings(), getCredentials()/getDecryptedToken() and getIntegrationId() for the fixed integration.
|
||||||
|
- Save updates the single integration row and single settings row; empty token preserves the old encrypted token.
|
||||||
|
- Controller index renders one page with the form and test panel; save/test redirect back to /settings/integrations/fakturownia.
|
||||||
|
- Remove or neutralize /new, /edit, /delete routes and links for Fakturownia.
|
||||||
|
- Hub row uses the single settings object and no longer displays instance counts.
|
||||||
|
Avoid: native alert()/confirm(), inline CSS in new view code, and SQL string concatenation.
|
||||||
|
</action>
|
||||||
|
<verify>C:\xampp\php\php.exe -l on touched PHP files; manually open /settings/integrations and /settings/integrations/fakturownia, save settings, and run connection test.</verify>
|
||||||
|
<done>AC-1 and AC-4 satisfied.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
<task type="auto">
|
||||||
|
<name>Task 3: Keep invoice config delegation compatible with the global integration</name>
|
||||||
|
<files>src/Modules/Settings/InvoiceConfigRepository.php, src/Modules/Settings/InvoiceConfigController.php, src/Modules/Accounting/InvoiceService.php, resources/views/settings/accounting-invoice-edit.php, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md</files>
|
||||||
|
<action>
|
||||||
|
Update invoice config handling so delegated configs always use the single Fakturownia integration.
|
||||||
|
- When saving delegated invoice configs, ignore user-supplied multi-account choice and resolve the global Fakturownia integration_id from the repository.
|
||||||
|
- Simplify accounting invoice edit UI so delegation does not show a multi-account selector; show a compact hint/status that Fakturownia must be configured globally.
|
||||||
|
- Ensure InvoiceService still reads config integration_id and retrieves credentials through the updated repository.
|
||||||
|
- Update docs and changelog with new single-instance contract and migration behavior.
|
||||||
|
Avoid: schema removal of invoice_configs.integration_id and behavior changes for local non-delegated invoice configs.
|
||||||
|
</action>
|
||||||
|
<verify>C:\xampp\php\php.exe -l on touched PHP files; create/edit local and delegated invoice configs; issue a delegated invoice against a configured Fakturownia account in manual UAT.</verify>
|
||||||
|
<done>AC-3 and AC-5 satisfied.</done>
|
||||||
|
</task>
|
||||||
|
|
||||||
|
</tasks>
|
||||||
|
|
||||||
|
<boundaries>
|
||||||
|
|
||||||
|
## DO NOT CHANGE
|
||||||
|
- Do not remove `invoice_configs.integration_id`.
|
||||||
|
- Do not change existing `invoices` rows or invoice PDF rendering.
|
||||||
|
- Do not add `invoice.created` automation event.
|
||||||
|
- Do not implement Fakturownia double-POST idempotency in this plan.
|
||||||
|
- Do not change HostedSMS/SMSPLANET behavior.
|
||||||
|
|
||||||
|
## SCOPE LIMITS
|
||||||
|
- This plan only changes Fakturownia account configuration from multi-instance to single-instance.
|
||||||
|
- Existing delegated invoice issuing remains functionally the same after it resolves the global account.
|
||||||
|
- Live Fakturownia API testing depends on operator credentials and network access.
|
||||||
|
|
||||||
|
</boundaries>
|
||||||
|
|
||||||
|
<verification>
|
||||||
|
Before declaring plan complete:
|
||||||
|
- [ ] `C:\xampp\php\php.exe bin\migrate.php`
|
||||||
|
- [ ] `C:\xampp\php\php.exe -l` for all touched PHP files
|
||||||
|
- [ ] Manual UI smoke: integrations hub, Fakturownia settings save, Fakturownia test
|
||||||
|
- [ ] Manual UI smoke: invoice config create/edit for local and delegated modes
|
||||||
|
- [ ] Delegated invoice issue still calls Fakturownia with the global account
|
||||||
|
- [ ] DOCS/DB_SCHEMA.md, DOCS/ARCHITECTURE.md, DOCS/TECH_CHANGELOG.md updated
|
||||||
|
- [ ] `sonar-scanner` run after APPLY if available in PATH
|
||||||
|
</verification>
|
||||||
|
|
||||||
|
<success_criteria>
|
||||||
|
- Fakturownia has exactly one configurable instance from the UI.
|
||||||
|
- Migration preserves one active Fakturownia row and rewires delegated invoice configs to it.
|
||||||
|
- No route or view offers adding/deleting multiple Fakturownia accounts.
|
||||||
|
- Delegated invoice configs and delegated invoice issuing remain compatible.
|
||||||
|
- All verification checks pass or environment-dependent gaps are documented in SUMMARY.md.
|
||||||
|
</success_criteria>
|
||||||
|
|
||||||
|
<output>
|
||||||
|
After completion, create `.paul/phases/118-fakturownia-single-instance/118-01-SUMMARY.md`.
|
||||||
|
</output>
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# Phase 118 Plan 01 Summary - Fakturownia Single Instance
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
APPLY complete on 2026-05-12 13:47. Ready for UNIFY.
|
||||||
|
|
||||||
|
## Implemented
|
||||||
|
|
||||||
|
- Added `database/migrations/20260512_000109_fakturownia_single_instance.sql`.
|
||||||
|
- Converted `FakturowniaIntegrationRepository` to one global settings row with `getSettings()`, `saveSettings()`, `getIntegrationId()`, `getCredentials()`, and compatibility wrappers.
|
||||||
|
- Simplified `FakturowniaIntegrationController` to one settings page plus save/test actions.
|
||||||
|
- Replaced Fakturownia multi-account list UI with a single configuration form and test panel.
|
||||||
|
- Updated integrations hub to show Fakturownia as one instance.
|
||||||
|
- Updated delegated invoice config save flow to always use the global Fakturownia `integration_id`.
|
||||||
|
- Removed the invoice config account selector and replaced it with global Fakturownia status.
|
||||||
|
- Updated `DOCS/DB_SCHEMA.md`, `DOCS/ARCHITECTURE.md`, and `DOCS/TECH_CHANGELOG.md`.
|
||||||
|
- Updated `.paul/codebase/db_schema.md`, `.paul/codebase/architecture.md`, and `.paul/codebase/tech_changelog.md`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- PASS: `C:\xampp\php\php.exe -l` for changed PHP classes and `routes/web.php`.
|
||||||
|
- PASS: `C:\xampp\php\php.exe -l` for changed PHP views.
|
||||||
|
- PASS: static search found no active UI links for adding/deleting Fakturownia accounts; legacy `/new` and `/edit` routes redirect to the global settings page.
|
||||||
|
- BLOCKED: `C:\xampp\php\php.exe bin\migrate.php` could not connect to local MySQL (`SQLSTATE[HY000] [2002]` target actively refused connection).
|
||||||
|
- SKIPPED: PHPUnit is not available in `vendor/bin`.
|
||||||
|
- SKIPPED: `sonar-scanner` is not available in PATH.
|
||||||
|
|
||||||
|
## Environment Gaps
|
||||||
|
|
||||||
|
- Run `C:\xampp\php\php.exe bin\migrate.php` after local MySQL/XAMPP is running.
|
||||||
|
- Manually verify `/settings/integrations`, `/settings/integrations/fakturownia`, save/test Fakturownia settings, and local/delegated invoice config edit.
|
||||||
|
- Live Fakturownia API test still requires valid credentials and network access.
|
||||||
|
|
||||||
|
## Files Changed
|
||||||
|
|
||||||
|
- `database/migrations/20260512_000109_fakturownia_single_instance.sql`
|
||||||
|
- `src/Modules/Settings/FakturowniaIntegrationRepository.php`
|
||||||
|
- `src/Modules/Settings/FakturowniaIntegrationController.php`
|
||||||
|
- `src/Modules/Settings/IntegrationsHubController.php`
|
||||||
|
- `src/Modules/Settings/InvoiceConfigRepository.php`
|
||||||
|
- `src/Modules/Settings/InvoiceConfigController.php`
|
||||||
|
- `src/Modules/Accounting/InvoiceService.php`
|
||||||
|
- `resources/views/settings/fakturownia.php`
|
||||||
|
- `resources/views/settings/fakturownia-edit.php`
|
||||||
|
- `resources/views/settings/accounting-invoice-edit.php`
|
||||||
|
- `routes/web.php`
|
||||||
|
- `DOCS/DB_SCHEMA.md`
|
||||||
|
- `DOCS/ARCHITECTURE.md`
|
||||||
|
- `DOCS/TECH_CHANGELOG.md`
|
||||||
|
- `.paul/codebase/db_schema.md`
|
||||||
|
- `.paul/codebase/architecture.md`
|
||||||
|
- `.paul/codebase/tech_changelog.md`
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
INSERT INTO `integrations` (`type`, `name`, `base_url`, `timeout_seconds`, `is_active`, `created_at`, `updated_at`)
|
||||||
|
SELECT 'fakturownia', 'Fakturownia', 'https://app.fakturownia.pl', 15, 1, NOW(), NOW()
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1 FROM `integrations` WHERE `type` = 'fakturownia'
|
||||||
|
);
|
||||||
|
|
||||||
|
SET @fakturownia_integration_id := (
|
||||||
|
SELECT `id`
|
||||||
|
FROM (
|
||||||
|
SELECT
|
||||||
|
i.`id`,
|
||||||
|
i.`is_active`,
|
||||||
|
COALESCE(usage_counts.`used_count`, 0) AS `used_count`
|
||||||
|
FROM `integrations` i
|
||||||
|
LEFT JOIN (
|
||||||
|
SELECT `integration_id`, COUNT(*) AS `used_count`
|
||||||
|
FROM `invoice_configs`
|
||||||
|
WHERE `integration_id` IS NOT NULL
|
||||||
|
GROUP BY `integration_id`
|
||||||
|
) usage_counts ON usage_counts.`integration_id` = i.`id`
|
||||||
|
WHERE i.`type` = 'fakturownia'
|
||||||
|
ORDER BY i.`is_active` DESC, COALESCE(usage_counts.`used_count`, 0) DESC, i.`id` ASC
|
||||||
|
LIMIT 1
|
||||||
|
) selected_fakturownia
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE `integrations`
|
||||||
|
SET `name` = CONCAT(`name`, ' #', `id`),
|
||||||
|
`updated_at` = NOW()
|
||||||
|
WHERE `type` = 'fakturownia'
|
||||||
|
AND `id` <> @fakturownia_integration_id
|
||||||
|
AND `name` = 'Fakturownia';
|
||||||
|
|
||||||
|
UPDATE `integrations`
|
||||||
|
SET `name` = 'Fakturownia',
|
||||||
|
`base_url` = 'https://app.fakturownia.pl',
|
||||||
|
`timeout_seconds` = 15,
|
||||||
|
`updated_at` = NOW()
|
||||||
|
WHERE `id` = @fakturownia_integration_id
|
||||||
|
AND `type` = 'fakturownia';
|
||||||
|
|
||||||
|
UPDATE `invoice_configs`
|
||||||
|
SET `integration_id` = @fakturownia_integration_id,
|
||||||
|
`updated_at` = NOW()
|
||||||
|
WHERE `is_delegated` = 1;
|
||||||
|
|
||||||
|
UPDATE `invoice_configs`
|
||||||
|
SET `integration_id` = NULL,
|
||||||
|
`updated_at` = NOW()
|
||||||
|
WHERE `is_delegated` = 0;
|
||||||
|
|
||||||
|
DELETE FROM `fakturownia_integration_settings`
|
||||||
|
WHERE `integration_id` <> @fakturownia_integration_id;
|
||||||
|
|
||||||
|
INSERT INTO `fakturownia_integration_settings`
|
||||||
|
(`id`, `integration_id`, `account_prefix`, `default_kind`, `default_payment_to_days`, `created_at`, `updated_at`)
|
||||||
|
SELECT 1, @fakturownia_integration_id, '', 'vat', 7, NOW(), NOW()
|
||||||
|
WHERE NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM `fakturownia_integration_settings`
|
||||||
|
WHERE `integration_id` = @fakturownia_integration_id
|
||||||
|
);
|
||||||
|
|
||||||
|
UPDATE `fakturownia_integration_settings`
|
||||||
|
SET `id` = 1,
|
||||||
|
`integration_id` = @fakturownia_integration_id,
|
||||||
|
`updated_at` = NOW()
|
||||||
|
WHERE `integration_id` = @fakturownia_integration_id;
|
||||||
|
|
||||||
|
DELETE FROM `integrations`
|
||||||
|
WHERE `type` = 'fakturownia'
|
||||||
|
AND `id` <> @fakturownia_integration_id;
|
||||||
|
|
||||||
|
ALTER TABLE `fakturownia_integration_settings` AUTO_INCREMENT = 2;
|
||||||
@@ -1,120 +1,9 @@
|
|||||||
<?php
|
<?php
|
||||||
/** @var array<string, mixed>|null $row */
|
|
||||||
$row = is_array($row ?? null) ? $row : null;
|
|
||||||
$isNew = $row === null;
|
|
||||||
$integrationId = (int) ($row['integration_id'] ?? 0);
|
|
||||||
$name = (string) ($row['name'] ?? '');
|
|
||||||
$prefix = (string) ($row['account_prefix'] ?? '');
|
|
||||||
$departmentId = (string) ($row['department_id'] ?? '');
|
|
||||||
$defaultKind = (string) ($row['default_kind'] ?? 'vat');
|
|
||||||
$defaultPaymentDays = (int) ($row['default_payment_to_days'] ?? 7);
|
|
||||||
$isActive = $isNew ? true : (bool) ($row['is_active'] ?? false);
|
|
||||||
$hasToken = (bool) ($row['has_api_token'] ?? false);
|
|
||||||
$lastTestAt = (string) ($row['last_test_at'] ?? '');
|
|
||||||
$lastTestStatus = (string) ($row['last_test_status'] ?? '');
|
|
||||||
$lastTestMessage = (string) ($row['last_test_message'] ?? '');
|
|
||||||
|
|
||||||
$flashSave = trim((string) ($flashSave ?? ''));
|
|
||||||
$flashTest = trim((string) ($flashTest ?? ''));
|
|
||||||
$flashError = trim((string) ($flashError ?? ''));
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<h2 class="section-title"><?= $isNew ? 'Nowa integracja Fakturownia' : 'Edycja integracji Fakturownia' ?></h2>
|
<h2 class="section-title">Integracja Fakturownia</h2>
|
||||||
<p class="muted mt-12">Wystawianie faktur w aplikacji Fakturownia (app.fakturownia.pl).</p>
|
<p class="muted mt-12">Fakturownia ma jedna globalna konfiguracje. Wroc do strony konfiguracji.</p>
|
||||||
|
<div class="form-actions mt-16">
|
||||||
<?php if ($flashError !== ''): ?>
|
<a class="btn btn--primary" href="/settings/integrations/fakturownia">Otworz konfiguracje</a>
|
||||||
<div class="alert alert--danger mt-12" role="alert"><?= $e($flashError) ?></div>
|
</div>
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($flashSave !== ''): ?>
|
|
||||||
<div class="alert alert--success mt-12" role="status"><?= $e($flashSave) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($flashTest !== ''): ?>
|
|
||||||
<div class="alert alert--info mt-12" role="status"><?= $e($flashTest) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="card mt-16">
|
|
||||||
<form class="statuses-form" action="/settings/integrations/fakturownia/save" method="post" novalidate>
|
|
||||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
|
||||||
<?php if (!$isNew): ?>
|
|
||||||
<input type="hidden" name="id" value="<?= $integrationId ?>">
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<label class="form-field">
|
|
||||||
<span class="field-label">Nazwa integracji *</span>
|
|
||||||
<input class="form-control" type="text" name="name" maxlength="128" value="<?= $e($name) ?>" required>
|
|
||||||
<span class="muted">Dowolna etykieta pomocnicza (np. "Fakturownia - moja firma").</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="form-field">
|
|
||||||
<span class="field-label">Prefix konta (subdomena) *</span>
|
|
||||||
<div style="display:flex;align-items:center;gap:8px;">
|
|
||||||
<input class="form-control" type="text" name="account_prefix" maxlength="63" value="<?= $e($prefix) ?>" pattern="[a-z0-9][a-z0-9-]{1,62}" required style="flex:0 0 220px;">
|
|
||||||
<span class="muted">.fakturownia.pl</span>
|
|
||||||
</div>
|
|
||||||
<span class="muted">Maly litery, cyfry, mysliniki. Tak jak w adresie panelu Fakturowni.</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="form-field">
|
|
||||||
<span class="field-label">Token API <?= $isNew ? '*' : '' ?></span>
|
|
||||||
<input class="form-control" type="password" name="api_token" autocomplete="new-password" placeholder="<?= $hasToken ? '********' : '' ?>" <?= $isNew ? 'required' : '' ?>>
|
|
||||||
<span class="muted"><?= $hasToken ? 'Token jest zapisany. Wpisz nowy aby nadpisac.' : 'Token API z Fakturowni (Ustawienia > Konta uzytkownikow > API).' ?></span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="form-field">
|
|
||||||
<span class="field-label">ID departamentu (opcjonalnie)</span>
|
|
||||||
<input class="form-control" type="text" name="department_id" maxlength="64" value="<?= $e($departmentId) ?>">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="form-field">
|
|
||||||
<span class="field-label">Domyslny typ dokumentu</span>
|
|
||||||
<select class="form-control" name="default_kind">
|
|
||||||
<option value="vat" <?= $defaultKind === 'vat' ? 'selected' : '' ?>>Faktura VAT</option>
|
|
||||||
<option value="proforma" <?= $defaultKind === 'proforma' ? 'selected' : '' ?>>Proforma</option>
|
|
||||||
<option value="invoice_other" <?= $defaultKind === 'invoice_other' ? 'selected' : '' ?>>Inna</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="form-field">
|
|
||||||
<span class="field-label">Domyslny termin platnosci (dni)</span>
|
|
||||||
<input class="form-control" type="number" name="default_payment_to_days" min="0" max="120" value="<?= $defaultPaymentDays ?>" style="max-width:120px;">
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label class="form-field">
|
|
||||||
<span class="field-label">Status</span>
|
|
||||||
<label style="display:inline-flex;align-items:center;gap:8px;">
|
|
||||||
<input type="checkbox" name="is_active" value="1" <?= $isActive ? 'checked' : '' ?>>
|
|
||||||
Integracja aktywna
|
|
||||||
</label>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div class="form-actions" style="display:flex;gap:8px;">
|
|
||||||
<button type="submit" class="btn btn--primary">Zapisz</button>
|
|
||||||
<a class="btn btn--secondary" href="/settings/integrations/fakturownia">Anuluj</a>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<?php if (!$isNew): ?>
|
|
||||||
<hr class="mt-16">
|
|
||||||
<h3 class="section-title">Test polaczenia</h3>
|
|
||||||
<p class="muted">Wykonuje GET <code><?= $e('https://' . ($prefix !== '' ? $prefix : '{prefix}') . '.fakturownia.pl/account.json') ?></code> z zapisanym tokenem.</p>
|
|
||||||
<form action="/settings/integrations/fakturownia/test" method="post" style="margin-top:12px;">
|
|
||||||
<input type="hidden" name="_token" value="<?= $e($csrfToken ?? '') ?>">
|
|
||||||
<input type="hidden" name="id" value="<?= $integrationId ?>">
|
|
||||||
<button type="submit" class="btn btn--secondary">Testuj polaczenie</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<?php if ($lastTestAt !== ''): ?>
|
|
||||||
<div class="muted mt-12">
|
|
||||||
Ostatni test: <strong><?= $e($lastTestAt) ?></strong>
|
|
||||||
<?php if ($lastTestStatus !== ''): ?>
|
|
||||||
— <strong><?= $e($lastTestStatus) ?></strong>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php if ($lastTestMessage !== ''): ?>
|
|
||||||
<div><?= $e($lastTestMessage) ?></div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
<?php endif; ?>
|
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -192,12 +192,12 @@ final class InvoiceService
|
|||||||
|
|
||||||
$account = $this->fakturownia->findByIntegrationId($integrationId);
|
$account = $this->fakturownia->findByIntegrationId($integrationId);
|
||||||
if ($account === null) {
|
if ($account === null) {
|
||||||
throw new InvoiceIssueException('Konto Fakturownia nie istnieje (id=' . $integrationId . ').');
|
throw new InvoiceIssueException('Globalna konfiguracja Fakturowni nie istnieje (id=' . $integrationId . ').');
|
||||||
}
|
}
|
||||||
|
|
||||||
$prefix = trim((string) ($account['account_prefix'] ?? ''));
|
$prefix = trim((string) ($account['account_prefix'] ?? ''));
|
||||||
if ($prefix === '') {
|
if ($prefix === '') {
|
||||||
throw new InvoiceIssueException('Konto Fakturownia nie ma ustawionego prefiksu (subdomeny).');
|
throw new InvoiceIssueException('Globalna konfiguracja Fakturowni nie ma ustawionego prefiksu (subdomeny).');
|
||||||
}
|
}
|
||||||
|
|
||||||
$apiToken = $this->fakturownia->getDecryptedToken($integrationId);
|
$apiToken = $this->fakturownia->getDecryptedToken($integrationId);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Modules\Settings;
|
namespace App\Modules\Settings;
|
||||||
|
|
||||||
use App\Core\Http\RedirectPathResolver;
|
|
||||||
use App\Core\Http\Request;
|
use App\Core\Http\Request;
|
||||||
use App\Core\Http\Response;
|
use App\Core\Http\Response;
|
||||||
use App\Core\I18n\Translator;
|
use App\Core\I18n\Translator;
|
||||||
@@ -27,15 +26,13 @@ final class FakturowniaIntegrationController
|
|||||||
|
|
||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
$rows = $this->repository->findAll();
|
|
||||||
|
|
||||||
$html = $this->template->render('settings/fakturownia', [
|
$html = $this->template->render('settings/fakturownia', [
|
||||||
'title' => 'Integracja Fakturownia',
|
'title' => 'Integracja Fakturownia',
|
||||||
'activeMenu' => 'settings',
|
'activeMenu' => 'settings',
|
||||||
'activeSettings' => 'integrations',
|
'activeSettings' => 'integrations',
|
||||||
'user' => $this->auth->user(),
|
'user' => $this->auth->user(),
|
||||||
'csrfToken' => Csrf::token(),
|
'csrfToken' => Csrf::token(),
|
||||||
'rows' => $rows,
|
'settings' => $this->repository->getSettings(),
|
||||||
'flashSave' => (string) Flash::get('fakturownia.save', ''),
|
'flashSave' => (string) Flash::get('fakturownia.save', ''),
|
||||||
'flashTest' => (string) Flash::get('fakturownia.test', ''),
|
'flashTest' => (string) Flash::get('fakturownia.test', ''),
|
||||||
'flashError' => (string) Flash::get('fakturownia.error', ''),
|
'flashError' => (string) Flash::get('fakturownia.error', ''),
|
||||||
@@ -46,34 +43,11 @@ final class FakturowniaIntegrationController
|
|||||||
|
|
||||||
public function edit(Request $request): Response
|
public function edit(Request $request): Response
|
||||||
{
|
{
|
||||||
$integrationId = (int) $request->input('id', 0);
|
return Response::redirect('/settings/integrations/fakturownia');
|
||||||
$row = $integrationId > 0 ? $this->repository->findByIntegrationId($integrationId) : null;
|
|
||||||
|
|
||||||
if ($integrationId > 0 && $row === null) {
|
|
||||||
Flash::set('fakturownia.error', 'Nie znaleziono integracji Fakturowni o ID ' . $integrationId . '.');
|
|
||||||
return Response::redirect('/settings/integrations/fakturownia');
|
|
||||||
}
|
|
||||||
|
|
||||||
$html = $this->template->render('settings/fakturownia-edit', [
|
|
||||||
'title' => $row === null
|
|
||||||
? 'Nowa integracja Fakturownia'
|
|
||||||
: 'Edycja integracji Fakturownia',
|
|
||||||
'activeMenu' => 'settings',
|
|
||||||
'activeSettings' => 'integrations',
|
|
||||||
'user' => $this->auth->user(),
|
|
||||||
'csrfToken' => Csrf::token(),
|
|
||||||
'row' => $row,
|
|
||||||
'flashSave' => (string) Flash::get('fakturownia.save', ''),
|
|
||||||
'flashTest' => (string) Flash::get('fakturownia.test', ''),
|
|
||||||
'flashError' => (string) Flash::get('fakturownia.error', ''),
|
|
||||||
], 'layouts/app');
|
|
||||||
|
|
||||||
return Response::html($html);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function save(Request $request): Response
|
public function save(Request $request): Response
|
||||||
{
|
{
|
||||||
$integrationId = (int) $request->input('id', 0);
|
|
||||||
$redirectTo = '/settings/integrations/fakturownia';
|
$redirectTo = '/settings/integrations/fakturownia';
|
||||||
|
|
||||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
@@ -82,29 +56,18 @@ final class FakturowniaIntegrationController
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->repository->save(
|
$this->repository->saveSettings([
|
||||||
$integrationId > 0 ? $integrationId : null,
|
'account_prefix' => (string) $request->input('account_prefix', ''),
|
||||||
[
|
'api_token' => (string) $request->input('api_token', ''),
|
||||||
'name' => (string) $request->input('name', ''),
|
'department_id' => (string) $request->input('department_id', ''),
|
||||||
'account_prefix' => (string) $request->input('account_prefix', ''),
|
'default_kind' => (string) $request->input('default_kind', 'vat'),
|
||||||
'api_token' => (string) $request->input('api_token', ''),
|
'default_payment_to_days' => (int) $request->input('default_payment_to_days', 7),
|
||||||
'department_id' => (string) $request->input('department_id', ''),
|
'is_active' => $request->input('is_active', ''),
|
||||||
'default_kind' => (string) $request->input('default_kind', 'vat'),
|
]);
|
||||||
'default_payment_to_days' => (int) $request->input('default_payment_to_days', 7),
|
|
||||||
'is_active' => $request->input('is_active', ''),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
Flash::set('fakturownia.save', 'Zapisano integracje Fakturowni.');
|
Flash::set('fakturownia.save', 'Zapisano konfiguracje Fakturowni.');
|
||||||
} catch (Throwable $exception) {
|
} catch (Throwable $exception) {
|
||||||
Flash::set('fakturownia.error', 'Nie udalo sie zapisac integracji: ' . $exception->getMessage());
|
Flash::set('fakturownia.error', 'Nie udalo sie zapisac konfiguracji: ' . $exception->getMessage());
|
||||||
return Response::redirect(RedirectPathResolver::resolve(
|
|
||||||
$integrationId > 0
|
|
||||||
? '/settings/integrations/fakturownia/edit?id=' . $integrationId
|
|
||||||
: '/settings/integrations/fakturownia/new',
|
|
||||||
['/settings/integrations/fakturownia'],
|
|
||||||
'/settings/integrations/fakturownia'
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Response::redirect($redirectTo);
|
return Response::redirect($redirectTo);
|
||||||
@@ -112,31 +75,19 @@ final class FakturowniaIntegrationController
|
|||||||
|
|
||||||
public function test(Request $request): Response
|
public function test(Request $request): Response
|
||||||
{
|
{
|
||||||
$integrationId = (int) $request->input('id', 0);
|
$redirectTo = '/settings/integrations/fakturownia';
|
||||||
$redirectTo = $integrationId > 0
|
|
||||||
? '/settings/integrations/fakturownia/edit?id=' . $integrationId
|
|
||||||
: '/settings/integrations/fakturownia';
|
|
||||||
|
|
||||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||||
Flash::set('fakturownia.error', $this->translator->get('auth.errors.csrf_expired'));
|
Flash::set('fakturownia.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||||
return Response::redirect($redirectTo);
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($integrationId <= 0) {
|
$settings = $this->repository->getSettings();
|
||||||
Flash::set('fakturownia.error', 'Najpierw zapisz integracje, potem przetestuj polaczenie.');
|
$integrationId = (int) ($settings['integration_id'] ?? 0);
|
||||||
return Response::redirect($redirectTo);
|
$prefix = (string) ($settings['account_prefix'] ?? '');
|
||||||
}
|
|
||||||
|
|
||||||
$row = $this->repository->findByIntegrationId($integrationId);
|
|
||||||
if ($row === null) {
|
|
||||||
Flash::set('fakturownia.error', 'Integracja nie istnieje.');
|
|
||||||
return Response::redirect('/settings/integrations/fakturownia');
|
|
||||||
}
|
|
||||||
|
|
||||||
$prefix = (string) ($row['account_prefix'] ?? '');
|
|
||||||
$token = $this->repository->getDecryptedToken($integrationId);
|
$token = $this->repository->getDecryptedToken($integrationId);
|
||||||
|
|
||||||
if ($prefix === '' || $token === null || $token === '') {
|
if ($integrationId <= 0 || $prefix === '' || $token === null || $token === '') {
|
||||||
Flash::set('fakturownia.test', 'Brak prefiksu lub tokenu - uzupelnij dane i zapisz przed testem.');
|
Flash::set('fakturownia.test', 'Brak prefiksu lub tokenu - uzupelnij dane i zapisz przed testem.');
|
||||||
$this->integrations->updateTestResult($integrationId, 'fail', 0, 'Brak prefiksu lub tokenu.');
|
$this->integrations->updateTestResult($integrationId, 'fail', 0, 'Brak prefiksu lub tokenu.');
|
||||||
return Response::redirect($redirectTo);
|
return Response::redirect($redirectTo);
|
||||||
@@ -151,36 +102,18 @@ final class FakturowniaIntegrationController
|
|||||||
(string) $result['message']
|
(string) $result['message']
|
||||||
);
|
);
|
||||||
|
|
||||||
$msg = $result['ok']
|
$message = $result['ok']
|
||||||
? 'OK (HTTP ' . (int) $result['http_code'] . ')'
|
? 'OK (HTTP ' . (int) $result['http_code'] . ')'
|
||||||
: 'BLAD: ' . $result['message'] . ' (HTTP ' . (int) $result['http_code'] . ')';
|
: 'BLAD: ' . $result['message'] . ' (HTTP ' . (int) $result['http_code'] . ')';
|
||||||
Flash::set('fakturownia.test', $msg);
|
Flash::set('fakturownia.test', $message);
|
||||||
|
|
||||||
return Response::redirect($redirectTo);
|
return Response::redirect($redirectTo);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function delete(Request $request): Response
|
public function delete(Request $request): Response
|
||||||
{
|
{
|
||||||
$integrationId = (int) $request->input('id', 0);
|
Flash::set('fakturownia.error', 'Fakturownia ma jedna globalna konfiguracje i nie moze byc usunieta.');
|
||||||
$redirectTo = '/settings/integrations/fakturownia';
|
|
||||||
|
|
||||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
return Response::redirect('/settings/integrations/fakturownia');
|
||||||
Flash::set('fakturownia.error', $this->translator->get('auth.errors.csrf_expired'));
|
|
||||||
return Response::redirect($redirectTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($integrationId <= 0) {
|
|
||||||
Flash::set('fakturownia.error', 'Brak identyfikatora integracji.');
|
|
||||||
return Response::redirect($redirectTo);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->repository->delete($integrationId);
|
|
||||||
Flash::set('fakturownia.save', 'Usunieto integracje Fakturowni.');
|
|
||||||
} catch (Throwable $exception) {
|
|
||||||
Flash::set('fakturownia.error', $exception->getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
return Response::redirect($redirectTo);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use Throwable;
|
|||||||
final class FakturowniaIntegrationRepository
|
final class FakturowniaIntegrationRepository
|
||||||
{
|
{
|
||||||
private const INTEGRATION_TYPE = 'fakturownia';
|
private const INTEGRATION_TYPE = 'fakturownia';
|
||||||
|
private const INTEGRATION_NAME = 'Fakturownia';
|
||||||
private const INTEGRATION_BASE_URL = 'https://app.fakturownia.pl';
|
private const INTEGRATION_BASE_URL = 'https://app.fakturownia.pl';
|
||||||
|
|
||||||
private readonly IntegrationsRepository $integrations;
|
private readonly IntegrationsRepository $integrations;
|
||||||
@@ -25,28 +26,44 @@ final class FakturowniaIntegrationRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function getSettings(): array
|
||||||
|
{
|
||||||
|
$this->ensureRow();
|
||||||
|
$integrationId = $this->getIntegrationId();
|
||||||
|
$row = $this->fetchSettingsRow();
|
||||||
|
$integration = $this->integrations->findById($integrationId);
|
||||||
|
$encryptedToken = $this->resolveApiTokenEncrypted($row, $integration);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'settings_id' => isset($row['id']) ? (int) $row['id'] : null,
|
||||||
|
'name' => (string) ($integration['name'] ?? self::INTEGRATION_NAME),
|
||||||
|
'is_active' => (int) ($integration['is_active'] ?? 1) === 1,
|
||||||
|
'account_prefix' => trim((string) ($row['account_prefix'] ?? '')),
|
||||||
|
'api_token_encrypted' => $encryptedToken,
|
||||||
|
'has_api_token' => $encryptedToken !== null && $encryptedToken !== '',
|
||||||
|
'department_id' => trim((string) ($row['department_id'] ?? '')),
|
||||||
|
'default_kind' => trim((string) ($row['default_kind'] ?? 'vat')),
|
||||||
|
'default_payment_to_days' => (int) ($row['default_payment_to_days'] ?? 7),
|
||||||
|
'last_test_status' => trim((string) ($integration['last_test_status'] ?? '')),
|
||||||
|
'last_test_http_code' => isset($integration['last_test_http_code'])
|
||||||
|
? (int) $integration['last_test_http_code']
|
||||||
|
: null,
|
||||||
|
'last_test_message' => trim((string) ($integration['last_test_message'] ?? '')),
|
||||||
|
'last_test_at' => trim((string) ($integration['last_test_at'] ?? '')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backward-compatible list API for callers that still expect accounts collection.
|
||||||
|
*
|
||||||
* @return array<int, array<string, mixed>>
|
* @return array<int, array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
public function findAll(): array
|
public function findAll(): array
|
||||||
{
|
{
|
||||||
try {
|
return [$this->getSettings()];
|
||||||
$statement = $this->pdo->prepare(
|
|
||||||
'SELECT i.id AS integration_id, i.name, i.is_active,
|
|
||||||
i.last_test_status, i.last_test_http_code, i.last_test_message, i.last_test_at,
|
|
||||||
s.id AS settings_id, s.account_prefix, s.api_token_encrypted,
|
|
||||||
s.department_id, s.default_kind, s.default_payment_to_days
|
|
||||||
FROM integrations i
|
|
||||||
LEFT JOIN fakturownia_integration_settings s ON s.integration_id = i.id
|
|
||||||
WHERE i.type = :type
|
|
||||||
ORDER BY i.id ASC'
|
|
||||||
);
|
|
||||||
$statement->execute(['type' => self::INTEGRATION_TYPE]);
|
|
||||||
$rows = $statement->fetchAll(PDO::FETCH_ASSOC);
|
|
||||||
} catch (Throwable) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return is_array($rows) ? array_map(fn (array $row) => $this->mapRow($row), $rows) : [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,41 +71,24 @@ final class FakturowniaIntegrationRepository
|
|||||||
*/
|
*/
|
||||||
public function findByIntegrationId(int $integrationId): ?array
|
public function findByIntegrationId(int $integrationId): ?array
|
||||||
{
|
{
|
||||||
if ($integrationId <= 0) {
|
$globalId = $this->getIntegrationId();
|
||||||
|
if ($integrationId <= 0 || $integrationId !== $globalId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
return $this->getSettings();
|
||||||
$statement = $this->pdo->prepare(
|
|
||||||
'SELECT i.id AS integration_id, i.name, i.is_active,
|
|
||||||
i.last_test_status, i.last_test_http_code, i.last_test_message, i.last_test_at,
|
|
||||||
s.id AS settings_id, s.account_prefix, s.api_token_encrypted,
|
|
||||||
s.department_id, s.default_kind, s.default_payment_to_days
|
|
||||||
FROM integrations i
|
|
||||||
LEFT JOIN fakturownia_integration_settings s ON s.integration_id = i.id
|
|
||||||
WHERE i.id = :id AND i.type = :type
|
|
||||||
LIMIT 1'
|
|
||||||
);
|
|
||||||
$statement->execute([
|
|
||||||
'id' => $integrationId,
|
|
||||||
'type' => self::INTEGRATION_TYPE,
|
|
||||||
]);
|
|
||||||
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
|
||||||
} catch (Throwable) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return is_array($row) ? $this->mapRow($row) : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<string, mixed> $payload
|
* @param array<string, mixed> $payload
|
||||||
*/
|
*/
|
||||||
public function save(?int $integrationId, array $payload): int
|
public function saveSettings(array $payload): void
|
||||||
{
|
{
|
||||||
$name = trim((string) ($payload['name'] ?? ''));
|
$this->ensureRow();
|
||||||
if ($name === '') {
|
$integrationId = $this->getIntegrationId();
|
||||||
throw new IntegrationConfigException('Nazwa integracji Fakturowni jest wymagana.');
|
$row = $this->fetchSettingsRow();
|
||||||
|
if ($row === null) {
|
||||||
|
throw new IntegrationConfigException('Brak rekordu konfiguracji Fakturowni.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$prefix = strtolower(trim((string) ($payload['account_prefix'] ?? '')));
|
$prefix = strtolower(trim((string) ($payload['account_prefix'] ?? '')));
|
||||||
@@ -96,130 +96,180 @@ final class FakturowniaIntegrationRepository
|
|||||||
throw new IntegrationConfigException('Prefix konta (subdomena) ma niepoprawny format.');
|
throw new IntegrationConfigException('Prefix konta (subdomena) ma niepoprawny format.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$isActive = !empty($payload['is_active']);
|
|
||||||
$defaultKind = trim((string) ($payload['default_kind'] ?? 'vat'));
|
$defaultKind = trim((string) ($payload['default_kind'] ?? 'vat'));
|
||||||
if ($defaultKind === '') {
|
if ($defaultKind === '') {
|
||||||
$defaultKind = 'vat';
|
$defaultKind = 'vat';
|
||||||
}
|
}
|
||||||
$defaultPaymentDays = max(0, (int) ($payload['default_payment_to_days'] ?? 7));
|
if (mb_strlen($defaultKind) > 32) {
|
||||||
$departmentId = StringHelper::nullableString(trim((string) ($payload['department_id'] ?? '')));
|
throw new IntegrationConfigException('Typ dokumentu jest za dlugi (max 32 znaki).');
|
||||||
|
|
||||||
if ($integrationId === null || $integrationId <= 0) {
|
|
||||||
$integrationId = $this->integrations->ensureIntegration(
|
|
||||||
self::INTEGRATION_TYPE,
|
|
||||||
$name,
|
|
||||||
self::INTEGRATION_BASE_URL,
|
|
||||||
15,
|
|
||||||
$isActive
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
$this->updateIntegrationRow($integrationId, $name, $isActive);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$current = $this->findByIntegrationId($integrationId);
|
$defaultPaymentDays = (int) ($payload['default_payment_to_days'] ?? 7);
|
||||||
$currentEncrypted = $current['api_token_encrypted'] ?? null;
|
if ($defaultPaymentDays < 0) {
|
||||||
|
$defaultPaymentDays = 0;
|
||||||
|
}
|
||||||
|
if ($defaultPaymentDays > 120) {
|
||||||
|
$defaultPaymentDays = 120;
|
||||||
|
}
|
||||||
|
|
||||||
|
$departmentId = StringHelper::nullableString(trim((string) ($payload['department_id'] ?? '')));
|
||||||
|
$currentEncrypted = $this->resolveApiTokenEncrypted($row, $this->integrations->findById($integrationId));
|
||||||
$apiToken = trim((string) ($payload['api_token'] ?? ''));
|
$apiToken = trim((string) ($payload['api_token'] ?? ''));
|
||||||
$nextEncrypted = $currentEncrypted;
|
$nextEncrypted = $currentEncrypted;
|
||||||
if ($apiToken !== '') {
|
if ($apiToken !== '') {
|
||||||
$nextEncrypted = $this->cipher->encrypt($apiToken);
|
$nextEncrypted = $this->cipher->encrypt($apiToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->integrations->updateApiKeyEncrypted($integrationId, $nextEncrypted);
|
if ($nextEncrypted === null || $nextEncrypted === '') {
|
||||||
|
throw new IntegrationConfigException('Podaj token API Fakturowni.');
|
||||||
if ($current === null || ($current['settings_id'] ?? null) === null) {
|
|
||||||
$insert = $this->pdo->prepare(
|
|
||||||
'INSERT INTO fakturownia_integration_settings
|
|
||||||
(integration_id, account_prefix, api_token_encrypted, department_id, default_kind, default_payment_to_days)
|
|
||||||
VALUES
|
|
||||||
(:integration_id, :account_prefix, :api_token_encrypted, :department_id, :default_kind, :default_payment_to_days)'
|
|
||||||
);
|
|
||||||
$insert->execute([
|
|
||||||
'integration_id' => $integrationId,
|
|
||||||
'account_prefix' => $prefix,
|
|
||||||
'api_token_encrypted' => StringHelper::nullableString((string) $nextEncrypted),
|
|
||||||
'department_id' => $departmentId,
|
|
||||||
'default_kind' => $defaultKind,
|
|
||||||
'default_payment_to_days' => $defaultPaymentDays,
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
$update = $this->pdo->prepare(
|
|
||||||
'UPDATE fakturownia_integration_settings
|
|
||||||
SET account_prefix = :account_prefix,
|
|
||||||
api_token_encrypted = :api_token_encrypted,
|
|
||||||
department_id = :department_id,
|
|
||||||
default_kind = :default_kind,
|
|
||||||
default_payment_to_days = :default_payment_to_days,
|
|
||||||
updated_at = NOW()
|
|
||||||
WHERE integration_id = :integration_id'
|
|
||||||
);
|
|
||||||
$update->execute([
|
|
||||||
'integration_id' => $integrationId,
|
|
||||||
'account_prefix' => $prefix,
|
|
||||||
'api_token_encrypted' => StringHelper::nullableString((string) $nextEncrypted),
|
|
||||||
'department_id' => $departmentId,
|
|
||||||
'default_kind' => $defaultKind,
|
|
||||||
'default_payment_to_days' => $defaultPaymentDays,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $integrationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function delete(int $integrationId): void
|
|
||||||
{
|
|
||||||
if ($integrationId <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->isUsedByInvoiceConfig($integrationId)) {
|
|
||||||
throw new IntegrationConfigException(
|
|
||||||
'Nie mozna usunac integracji Fakturowni - jest uzywana przez konfiguracje faktur (invoice_configs).'
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$statement = $this->pdo->prepare(
|
$statement = $this->pdo->prepare(
|
||||||
'DELETE FROM integrations WHERE id = :id AND type = :type'
|
'UPDATE fakturownia_integration_settings
|
||||||
|
SET account_prefix = :account_prefix,
|
||||||
|
api_token_encrypted = :api_token_encrypted,
|
||||||
|
department_id = :department_id,
|
||||||
|
default_kind = :default_kind,
|
||||||
|
default_payment_to_days = :default_payment_to_days,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = 1'
|
||||||
);
|
);
|
||||||
$statement->execute([
|
$statement->execute([
|
||||||
'id' => $integrationId,
|
'account_prefix' => $prefix,
|
||||||
'type' => self::INTEGRATION_TYPE,
|
'api_token_encrypted' => $nextEncrypted,
|
||||||
|
'department_id' => $departmentId,
|
||||||
|
'default_kind' => $defaultKind,
|
||||||
|
'default_payment_to_days' => $defaultPaymentDays,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$this->updateIntegrationActive($integrationId, !empty($payload['is_active']));
|
||||||
|
$this->integrations->updateApiKeyEncrypted($integrationId, $nextEncrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compatibility wrapper for old callers.
|
||||||
|
*
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function save(?int $integrationId, array $payload): int
|
||||||
|
{
|
||||||
|
$this->saveSettings($payload);
|
||||||
|
|
||||||
|
return $this->getIntegrationId();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{integration_id: int, account_prefix: string, api_token: string, department_id: string, default_kind: string, default_payment_to_days: int}|null
|
||||||
|
*/
|
||||||
|
public function getCredentials(): ?array
|
||||||
|
{
|
||||||
|
$settings = $this->getSettings();
|
||||||
|
$integrationId = (int) ($settings['integration_id'] ?? 0);
|
||||||
|
$prefix = trim((string) ($settings['account_prefix'] ?? ''));
|
||||||
|
$encrypted = $settings['api_token_encrypted'] ?? null;
|
||||||
|
if ($integrationId <= 0 || $prefix === '' || !is_string($encrypted) || trim($encrypted) === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$token = trim($this->cipher->decrypt($encrypted));
|
||||||
|
if ($token === '') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'integration_id' => $integrationId,
|
||||||
|
'account_prefix' => $prefix,
|
||||||
|
'api_token' => $token,
|
||||||
|
'department_id' => trim((string) ($settings['department_id'] ?? '')),
|
||||||
|
'default_kind' => trim((string) ($settings['default_kind'] ?? 'vat')),
|
||||||
|
'default_payment_to_days' => (int) ($settings['default_payment_to_days'] ?? 7),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getIntegrationId(): int
|
||||||
|
{
|
||||||
|
$existing = $this->integrations->findFirstByType(self::INTEGRATION_TYPE);
|
||||||
|
if ($existing !== null) {
|
||||||
|
return (int) ($existing['id'] ?? 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->integrations->ensureIntegration(
|
||||||
|
self::INTEGRATION_TYPE,
|
||||||
|
self::INTEGRATION_NAME,
|
||||||
|
self::INTEGRATION_BASE_URL,
|
||||||
|
15,
|
||||||
|
true
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getDecryptedToken(int $integrationId): ?string
|
public function getDecryptedToken(int $integrationId): ?string
|
||||||
{
|
{
|
||||||
$row = $this->findByIntegrationId($integrationId);
|
$globalId = $this->getIntegrationId();
|
||||||
if ($row === null) {
|
if ($integrationId !== $globalId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$encrypted = $row['api_token_encrypted'] ?? null;
|
$credentials = $this->getCredentials();
|
||||||
if (!is_string($encrypted) || $encrypted === '') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->cipher->decrypt($encrypted);
|
return $credentials['api_token'] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function isUsedByInvoiceConfig(int $integrationId): bool
|
public function delete(int $integrationId): void
|
||||||
|
{
|
||||||
|
throw new IntegrationConfigException('Fakturownia ma jedna globalna konfiguracje i nie moze byc usunieta z UI.');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function ensureRow(): void
|
||||||
|
{
|
||||||
|
$integrationId = $this->getIntegrationId();
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'INSERT INTO fakturownia_integration_settings
|
||||||
|
(id, integration_id, account_prefix, default_kind, default_payment_to_days, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
(1, :integration_id, "", "vat", 7, NOW(), NOW())
|
||||||
|
ON DUPLICATE KEY UPDATE integration_id = VALUES(integration_id), updated_at = VALUES(updated_at)'
|
||||||
|
);
|
||||||
|
$statement->execute(['integration_id' => $integrationId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private function fetchSettingsRow(): ?array
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$statement = $this->pdo->prepare(
|
$statement = $this->pdo->prepare('SELECT * FROM fakturownia_integration_settings WHERE id = 1 LIMIT 1');
|
||||||
'SELECT 1 FROM invoice_configs WHERE integration_id = :id LIMIT 1'
|
$statement->execute();
|
||||||
);
|
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||||
$statement->execute(['id' => $integrationId]);
|
|
||||||
return $statement->fetchColumn() !== false;
|
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
return false;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return is_array($row) ? $row : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updateIntegrationRow(int $integrationId, string $name, bool $isActive): void
|
/**
|
||||||
|
* @param array<string, mixed>|null $row
|
||||||
|
* @param array<string, mixed>|null $integration
|
||||||
|
*/
|
||||||
|
private function resolveApiTokenEncrypted(?array $row, ?array $integration): ?string
|
||||||
|
{
|
||||||
|
$settingsValue = trim((string) ($row['api_token_encrypted'] ?? ''));
|
||||||
|
if ($settingsValue !== '') {
|
||||||
|
return $settingsValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$baseValue = trim((string) ($integration['api_key_encrypted'] ?? ''));
|
||||||
|
return StringHelper::nullableString($baseValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function updateIntegrationActive(int $integrationId, bool $isActive): void
|
||||||
{
|
{
|
||||||
$statement = $this->pdo->prepare(
|
$statement = $this->pdo->prepare(
|
||||||
'UPDATE integrations
|
'UPDATE integrations
|
||||||
SET name = :name,
|
SET name = :name,
|
||||||
|
base_url = :base_url,
|
||||||
|
timeout_seconds = :timeout_seconds,
|
||||||
is_active = :is_active,
|
is_active = :is_active,
|
||||||
updated_at = NOW()
|
updated_at = NOW()
|
||||||
WHERE id = :id AND type = :type'
|
WHERE id = :id AND type = :type'
|
||||||
@@ -227,43 +277,10 @@ final class FakturowniaIntegrationRepository
|
|||||||
$statement->execute([
|
$statement->execute([
|
||||||
'id' => $integrationId,
|
'id' => $integrationId,
|
||||||
'type' => self::INTEGRATION_TYPE,
|
'type' => self::INTEGRATION_TYPE,
|
||||||
'name' => $name,
|
'name' => self::INTEGRATION_NAME,
|
||||||
|
'base_url' => self::INTEGRATION_BASE_URL,
|
||||||
|
'timeout_seconds' => 15,
|
||||||
'is_active' => $isActive ? 1 : 0,
|
'is_active' => $isActive ? 1 : 0,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array<string, mixed> $row
|
|
||||||
* @return array<string, mixed>
|
|
||||||
*/
|
|
||||||
private function mapRow(array $row): array
|
|
||||||
{
|
|
||||||
$integrationId = (int) ($row['integration_id'] ?? 0);
|
|
||||||
$baseEncrypted = $this->integrations->getApiKeyEncrypted($integrationId);
|
|
||||||
$settingsEncrypted = isset($row['api_token_encrypted']) ? trim((string) $row['api_token_encrypted']) : '';
|
|
||||||
|
|
||||||
$resolvedEncrypted = null;
|
|
||||||
if ($baseEncrypted !== null && $baseEncrypted !== '') {
|
|
||||||
$resolvedEncrypted = $baseEncrypted;
|
|
||||||
} elseif ($settingsEncrypted !== '') {
|
|
||||||
$resolvedEncrypted = $settingsEncrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'integration_id' => $integrationId,
|
|
||||||
'settings_id' => isset($row['settings_id']) ? (int) $row['settings_id'] : null,
|
|
||||||
'name' => (string) ($row['name'] ?? ''),
|
|
||||||
'is_active' => (bool) ($row['is_active'] ?? false),
|
|
||||||
'account_prefix' => (string) ($row['account_prefix'] ?? ''),
|
|
||||||
'api_token_encrypted' => $resolvedEncrypted,
|
|
||||||
'has_api_token' => $resolvedEncrypted !== null && $resolvedEncrypted !== '',
|
|
||||||
'department_id' => isset($row['department_id']) ? (string) $row['department_id'] : '',
|
|
||||||
'default_kind' => (string) ($row['default_kind'] ?? 'vat'),
|
|
||||||
'default_payment_to_days' => (int) ($row['default_payment_to_days'] ?? 7),
|
|
||||||
'last_test_status' => isset($row['last_test_status']) ? (string) $row['last_test_status'] : '',
|
|
||||||
'last_test_http_code' => isset($row['last_test_http_code']) ? (int) $row['last_test_http_code'] : null,
|
|
||||||
'last_test_message' => isset($row['last_test_message']) ? (string) $row['last_test_message'] : '',
|
|
||||||
'last_test_at' => isset($row['last_test_at']) ? (string) $row['last_test_at'] : '',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -178,41 +178,21 @@ final class IntegrationsHubController
|
|||||||
*/
|
*/
|
||||||
private function buildFakturowniaRow(): array
|
private function buildFakturowniaRow(): array
|
||||||
{
|
{
|
||||||
$rows = $this->fakturownia->findAll();
|
$settings = $this->fakturownia->getSettings();
|
||||||
$instancesCount = count($rows);
|
$isConfigured = trim((string) ($settings['account_prefix'] ?? '')) !== ''
|
||||||
$activeCount = 0;
|
&& !empty($settings['has_api_token']);
|
||||||
$configuredCount = 0;
|
|
||||||
$lastTestAt = '';
|
|
||||||
|
|
||||||
foreach ($rows as $row) {
|
|
||||||
if (!empty($row['is_active'])) {
|
|
||||||
$activeCount++;
|
|
||||||
}
|
|
||||||
if (!empty($row['has_api_token'])) {
|
|
||||||
$configuredCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
$testedAt = trim((string) ($row['last_test_at'] ?? ''));
|
|
||||||
if ($testedAt !== '' && ($lastTestAt === '' || strcmp($testedAt, $lastTestAt) > 0)) {
|
|
||||||
$lastTestAt = $testedAt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$instanceLabel = $instancesCount > 0
|
|
||||||
? 'Fakturownia (' . $instancesCount . ')'
|
|
||||||
: 'Fakturownia';
|
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'provider' => 'Fakturownia',
|
'provider' => 'Fakturownia',
|
||||||
'instance' => $instanceLabel,
|
'instance' => 'Fakturownia',
|
||||||
'authorization_status' => $configuredCount > 0
|
'authorization_status' => $isConfigured
|
||||||
? $this->translator->get('settings.integrations_hub.status.configured')
|
? $this->translator->get('settings.integrations_hub.status.configured')
|
||||||
: $this->translator->get('settings.integrations_hub.status.not_configured'),
|
: $this->translator->get('settings.integrations_hub.status.not_configured'),
|
||||||
'secret_status' => $configuredCount > 0
|
'secret_status' => !empty($settings['has_api_token'])
|
||||||
? $this->translator->get('settings.integrations_hub.status.saved')
|
? $this->translator->get('settings.integrations_hub.status.saved')
|
||||||
: $this->translator->get('settings.integrations_hub.status.missing'),
|
: $this->translator->get('settings.integrations_hub.status.missing'),
|
||||||
'is_active' => $activeCount > 0,
|
'is_active' => !empty($settings['is_active']),
|
||||||
'last_test_at' => $lastTestAt,
|
'last_test_at' => trim((string) ($settings['last_test_at'] ?? '')),
|
||||||
'configure_url' => '/settings/integrations/fakturownia',
|
'configure_url' => '/settings/integrations/fakturownia',
|
||||||
'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'),
|
'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ final class InvoiceConfigController
|
|||||||
public function index(Request $request): Response
|
public function index(Request $request): Response
|
||||||
{
|
{
|
||||||
$configs = $this->repository->listAll();
|
$configs = $this->repository->listAll();
|
||||||
$accounts = $this->fakturownia->findAll();
|
$settings = $this->fakturownia->getSettings();
|
||||||
|
|
||||||
$html = $this->template->render('settings/accounting-invoices', [
|
$html = $this->template->render('settings/accounting-invoices', [
|
||||||
'title' => 'Konfiguracje faktur',
|
'title' => 'Konfiguracje faktur',
|
||||||
@@ -35,7 +35,7 @@ final class InvoiceConfigController
|
|||||||
'user' => $this->auth->user(),
|
'user' => $this->auth->user(),
|
||||||
'csrfToken' => Csrf::token(),
|
'csrfToken' => Csrf::token(),
|
||||||
'configs' => $configs,
|
'configs' => $configs,
|
||||||
'fakturowniaAccounts' => $accounts,
|
'fakturowniaSettings' => $settings,
|
||||||
'successMessage' => (string) Flash::get('accounting.invoices.save', ''),
|
'successMessage' => (string) Flash::get('accounting.invoices.save', ''),
|
||||||
'errorMessage' => (string) Flash::get('accounting.invoices.error', ''),
|
'errorMessage' => (string) Flash::get('accounting.invoices.error', ''),
|
||||||
], 'layouts/app');
|
], 'layouts/app');
|
||||||
@@ -53,10 +53,7 @@ final class InvoiceConfigController
|
|||||||
return Response::redirect('/settings/accounting/invoices');
|
return Response::redirect('/settings/accounting/invoices');
|
||||||
}
|
}
|
||||||
|
|
||||||
$accounts = array_values(array_filter(
|
$settings = $this->fakturownia->getSettings();
|
||||||
$this->fakturownia->findAll(),
|
|
||||||
static fn (array $row) => !empty($row['is_active'])
|
|
||||||
));
|
|
||||||
|
|
||||||
$html = $this->template->render('settings/accounting-invoice-edit', [
|
$html = $this->template->render('settings/accounting-invoice-edit', [
|
||||||
'title' => $config === null ? 'Nowa konfiguracja faktury' : 'Edycja konfiguracji faktury',
|
'title' => $config === null ? 'Nowa konfiguracja faktury' : 'Edycja konfiguracji faktury',
|
||||||
@@ -65,7 +62,7 @@ final class InvoiceConfigController
|
|||||||
'user' => $this->auth->user(),
|
'user' => $this->auth->user(),
|
||||||
'csrfToken' => Csrf::token(),
|
'csrfToken' => Csrf::token(),
|
||||||
'config' => $config,
|
'config' => $config,
|
||||||
'fakturowniaAccounts' => $accounts,
|
'fakturowniaSettings' => $settings,
|
||||||
'successMessage' => (string) Flash::get('accounting.invoices.save', ''),
|
'successMessage' => (string) Flash::get('accounting.invoices.save', ''),
|
||||||
'errorMessage' => (string) Flash::get('accounting.invoices.error', ''),
|
'errorMessage' => (string) Flash::get('accounting.invoices.error', ''),
|
||||||
], 'layouts/app');
|
], 'layouts/app');
|
||||||
@@ -96,7 +93,7 @@ final class InvoiceConfigController
|
|||||||
'payment_to_days' => (int) $request->input('payment_to_days', 7),
|
'payment_to_days' => (int) $request->input('payment_to_days', 7),
|
||||||
'default_kind' => (string) $request->input('default_kind', 'vat'),
|
'default_kind' => (string) $request->input('default_kind', 'vat'),
|
||||||
'is_delegated' => $request->input('is_delegated', ''),
|
'is_delegated' => $request->input('is_delegated', ''),
|
||||||
'integration_id' => $request->input('integration_id', ''),
|
'integration_id' => $this->fakturownia->getIntegrationId(),
|
||||||
'is_active' => $request->input('is_active', ''),
|
'is_active' => $request->input('is_active', ''),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ final class InvoiceConfigRepository
|
|||||||
{
|
{
|
||||||
use ToggleableRepositoryTrait;
|
use ToggleableRepositoryTrait;
|
||||||
|
|
||||||
public function __construct(private readonly PDO $pdo)
|
public function __construct(
|
||||||
|
private readonly PDO $pdo,
|
||||||
|
private readonly ?FakturowniaIntegrationRepository $fakturownia = null
|
||||||
|
)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,9 +119,10 @@ final class InvoiceConfigRepository
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
if ($isDelegated === 1) {
|
if ($isDelegated === 1) {
|
||||||
if ($integrationId === null || $integrationId <= 0) {
|
$integrationId = $this->globalFakturowniaIntegrationId();
|
||||||
|
if ($integrationId <= 0) {
|
||||||
throw new IntegrationConfigException(
|
throw new IntegrationConfigException(
|
||||||
'Przy delegacji wystawiania do Fakturowni musisz wskazac konto Fakturowni.'
|
'Przed delegacja faktur skonfiguruj globalna integracje Fakturownia.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!$this->isFakturowniaIntegration($integrationId)) {
|
if (!$this->isFakturowniaIntegration($integrationId)) {
|
||||||
@@ -213,6 +217,25 @@ final class InvoiceConfigRepository
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function globalFakturowniaIntegrationId(): int
|
||||||
|
{
|
||||||
|
if ($this->fakturownia !== null) {
|
||||||
|
return $this->fakturownia->getIntegrationId();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$statement = $this->pdo->prepare(
|
||||||
|
'SELECT id FROM integrations WHERE type = :type ORDER BY id ASC LIMIT 1'
|
||||||
|
);
|
||||||
|
$statement->execute(['type' => 'fakturownia']);
|
||||||
|
$value = $statement->fetchColumn();
|
||||||
|
} catch (Throwable) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return is_numeric($value) ? (int) $value : 0;
|
||||||
|
}
|
||||||
|
|
||||||
private function hasInvoices(int $configId): bool
|
private function hasInvoices(int $configId): bool
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user