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:
@@ -192,12 +192,12 @@ final class InvoiceService
|
||||
|
||||
$account = $this->fakturownia->findByIntegrationId($integrationId);
|
||||
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'] ?? ''));
|
||||
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);
|
||||
|
||||
@@ -3,7 +3,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Modules\Settings;
|
||||
|
||||
use App\Core\Http\RedirectPathResolver;
|
||||
use App\Core\Http\Request;
|
||||
use App\Core\Http\Response;
|
||||
use App\Core\I18n\Translator;
|
||||
@@ -27,15 +26,13 @@ final class FakturowniaIntegrationController
|
||||
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$rows = $this->repository->findAll();
|
||||
|
||||
$html = $this->template->render('settings/fakturownia', [
|
||||
'title' => 'Integracja Fakturownia',
|
||||
'activeMenu' => 'settings',
|
||||
'activeSettings' => 'integrations',
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'rows' => $rows,
|
||||
'settings' => $this->repository->getSettings(),
|
||||
'flashSave' => (string) Flash::get('fakturownia.save', ''),
|
||||
'flashTest' => (string) Flash::get('fakturownia.test', ''),
|
||||
'flashError' => (string) Flash::get('fakturownia.error', ''),
|
||||
@@ -46,34 +43,11 @@ final class FakturowniaIntegrationController
|
||||
|
||||
public function edit(Request $request): Response
|
||||
{
|
||||
$integrationId = (int) $request->input('id', 0);
|
||||
$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);
|
||||
return Response::redirect('/settings/integrations/fakturownia');
|
||||
}
|
||||
|
||||
public function save(Request $request): Response
|
||||
{
|
||||
$integrationId = (int) $request->input('id', 0);
|
||||
$redirectTo = '/settings/integrations/fakturownia';
|
||||
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
@@ -82,29 +56,18 @@ final class FakturowniaIntegrationController
|
||||
}
|
||||
|
||||
try {
|
||||
$this->repository->save(
|
||||
$integrationId > 0 ? $integrationId : null,
|
||||
[
|
||||
'name' => (string) $request->input('name', ''),
|
||||
'account_prefix' => (string) $request->input('account_prefix', ''),
|
||||
'api_token' => (string) $request->input('api_token', ''),
|
||||
'department_id' => (string) $request->input('department_id', ''),
|
||||
'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', ''),
|
||||
]
|
||||
);
|
||||
$this->repository->saveSettings([
|
||||
'account_prefix' => (string) $request->input('account_prefix', ''),
|
||||
'api_token' => (string) $request->input('api_token', ''),
|
||||
'department_id' => (string) $request->input('department_id', ''),
|
||||
'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) {
|
||||
Flash::set('fakturownia.error', 'Nie udalo sie zapisac integracji: ' . $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'
|
||||
));
|
||||
Flash::set('fakturownia.error', 'Nie udalo sie zapisac konfiguracji: ' . $exception->getMessage());
|
||||
}
|
||||
|
||||
return Response::redirect($redirectTo);
|
||||
@@ -112,31 +75,19 @@ final class FakturowniaIntegrationController
|
||||
|
||||
public function test(Request $request): Response
|
||||
{
|
||||
$integrationId = (int) $request->input('id', 0);
|
||||
$redirectTo = $integrationId > 0
|
||||
? '/settings/integrations/fakturownia/edit?id=' . $integrationId
|
||||
: '/settings/integrations/fakturownia';
|
||||
$redirectTo = '/settings/integrations/fakturownia';
|
||||
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
Flash::set('fakturownia.error', $this->translator->get('auth.errors.csrf_expired'));
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
if ($integrationId <= 0) {
|
||||
Flash::set('fakturownia.error', 'Najpierw zapisz integracje, potem przetestuj polaczenie.');
|
||||
return Response::redirect($redirectTo);
|
||||
}
|
||||
|
||||
$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'] ?? '');
|
||||
$settings = $this->repository->getSettings();
|
||||
$integrationId = (int) ($settings['integration_id'] ?? 0);
|
||||
$prefix = (string) ($settings['account_prefix'] ?? '');
|
||||
$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.');
|
||||
$this->integrations->updateTestResult($integrationId, 'fail', 0, 'Brak prefiksu lub tokenu.');
|
||||
return Response::redirect($redirectTo);
|
||||
@@ -151,36 +102,18 @@ final class FakturowniaIntegrationController
|
||||
(string) $result['message']
|
||||
);
|
||||
|
||||
$msg = $result['ok']
|
||||
$message = $result['ok']
|
||||
? 'OK (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);
|
||||
}
|
||||
|
||||
public function delete(Request $request): Response
|
||||
{
|
||||
$integrationId = (int) $request->input('id', 0);
|
||||
$redirectTo = '/settings/integrations/fakturownia';
|
||||
Flash::set('fakturownia.error', 'Fakturownia ma jedna globalna konfiguracje i nie moze byc usunieta.');
|
||||
|
||||
if (!Csrf::validate((string) $request->input('_token', ''))) {
|
||||
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);
|
||||
return Response::redirect('/settings/integrations/fakturownia');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ use Throwable;
|
||||
final class FakturowniaIntegrationRepository
|
||||
{
|
||||
private const INTEGRATION_TYPE = 'fakturownia';
|
||||
private const INTEGRATION_NAME = 'Fakturownia';
|
||||
private const INTEGRATION_BASE_URL = 'https://app.fakturownia.pl';
|
||||
|
||||
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>>
|
||||
*/
|
||||
public function findAll(): array
|
||||
{
|
||||
try {
|
||||
$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) : [];
|
||||
return [$this->getSettings()];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,41 +71,24 @@ final class FakturowniaIntegrationRepository
|
||||
*/
|
||||
public function findByIntegrationId(int $integrationId): ?array
|
||||
{
|
||||
if ($integrationId <= 0) {
|
||||
$globalId = $this->getIntegrationId();
|
||||
if ($integrationId <= 0 || $integrationId !== $globalId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$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;
|
||||
return $this->getSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
public function save(?int $integrationId, array $payload): int
|
||||
public function saveSettings(array $payload): void
|
||||
{
|
||||
$name = trim((string) ($payload['name'] ?? ''));
|
||||
if ($name === '') {
|
||||
throw new IntegrationConfigException('Nazwa integracji Fakturowni jest wymagana.');
|
||||
$this->ensureRow();
|
||||
$integrationId = $this->getIntegrationId();
|
||||
$row = $this->fetchSettingsRow();
|
||||
if ($row === null) {
|
||||
throw new IntegrationConfigException('Brak rekordu konfiguracji Fakturowni.');
|
||||
}
|
||||
|
||||
$prefix = strtolower(trim((string) ($payload['account_prefix'] ?? '')));
|
||||
@@ -96,130 +96,180 @@ final class FakturowniaIntegrationRepository
|
||||
throw new IntegrationConfigException('Prefix konta (subdomena) ma niepoprawny format.');
|
||||
}
|
||||
|
||||
$isActive = !empty($payload['is_active']);
|
||||
$defaultKind = trim((string) ($payload['default_kind'] ?? 'vat'));
|
||||
if ($defaultKind === '') {
|
||||
$defaultKind = 'vat';
|
||||
}
|
||||
$defaultPaymentDays = max(0, (int) ($payload['default_payment_to_days'] ?? 7));
|
||||
$departmentId = StringHelper::nullableString(trim((string) ($payload['department_id'] ?? '')));
|
||||
|
||||
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);
|
||||
if (mb_strlen($defaultKind) > 32) {
|
||||
throw new IntegrationConfigException('Typ dokumentu jest za dlugi (max 32 znaki).');
|
||||
}
|
||||
|
||||
$current = $this->findByIntegrationId($integrationId);
|
||||
$currentEncrypted = $current['api_token_encrypted'] ?? null;
|
||||
$defaultPaymentDays = (int) ($payload['default_payment_to_days'] ?? 7);
|
||||
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'] ?? ''));
|
||||
$nextEncrypted = $currentEncrypted;
|
||||
if ($apiToken !== '') {
|
||||
$nextEncrypted = $this->cipher->encrypt($apiToken);
|
||||
}
|
||||
|
||||
$this->integrations->updateApiKeyEncrypted($integrationId, $nextEncrypted);
|
||||
|
||||
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).'
|
||||
);
|
||||
if ($nextEncrypted === null || $nextEncrypted === '') {
|
||||
throw new IntegrationConfigException('Podaj token API Fakturowni.');
|
||||
}
|
||||
|
||||
$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([
|
||||
'id' => $integrationId,
|
||||
'type' => self::INTEGRATION_TYPE,
|
||||
'account_prefix' => $prefix,
|
||||
'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
|
||||
{
|
||||
$row = $this->findByIntegrationId($integrationId);
|
||||
if ($row === null) {
|
||||
$globalId = $this->getIntegrationId();
|
||||
if ($integrationId !== $globalId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$encrypted = $row['api_token_encrypted'] ?? null;
|
||||
if (!is_string($encrypted) || $encrypted === '') {
|
||||
return null;
|
||||
}
|
||||
$credentials = $this->getCredentials();
|
||||
|
||||
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 {
|
||||
$statement = $this->pdo->prepare(
|
||||
'SELECT 1 FROM invoice_configs WHERE integration_id = :id LIMIT 1'
|
||||
);
|
||||
$statement->execute(['id' => $integrationId]);
|
||||
return $statement->fetchColumn() !== false;
|
||||
$statement = $this->pdo->prepare('SELECT * FROM fakturownia_integration_settings WHERE id = 1 LIMIT 1');
|
||||
$statement->execute();
|
||||
$row = $statement->fetch(PDO::FETCH_ASSOC);
|
||||
} 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(
|
||||
'UPDATE integrations
|
||||
SET name = :name,
|
||||
base_url = :base_url,
|
||||
timeout_seconds = :timeout_seconds,
|
||||
is_active = :is_active,
|
||||
updated_at = NOW()
|
||||
WHERE id = :id AND type = :type'
|
||||
@@ -227,43 +277,10 @@ final class FakturowniaIntegrationRepository
|
||||
$statement->execute([
|
||||
'id' => $integrationId,
|
||||
'type' => self::INTEGRATION_TYPE,
|
||||
'name' => $name,
|
||||
'name' => self::INTEGRATION_NAME,
|
||||
'base_url' => self::INTEGRATION_BASE_URL,
|
||||
'timeout_seconds' => 15,
|
||||
'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
|
||||
{
|
||||
$rows = $this->fakturownia->findAll();
|
||||
$instancesCount = count($rows);
|
||||
$activeCount = 0;
|
||||
$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';
|
||||
$settings = $this->fakturownia->getSettings();
|
||||
$isConfigured = trim((string) ($settings['account_prefix'] ?? '')) !== ''
|
||||
&& !empty($settings['has_api_token']);
|
||||
|
||||
return [
|
||||
'provider' => 'Fakturownia',
|
||||
'instance' => $instanceLabel,
|
||||
'authorization_status' => $configuredCount > 0
|
||||
'instance' => 'Fakturownia',
|
||||
'authorization_status' => $isConfigured
|
||||
? $this->translator->get('settings.integrations_hub.status.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.missing'),
|
||||
'is_active' => $activeCount > 0,
|
||||
'last_test_at' => $lastTestAt,
|
||||
'is_active' => !empty($settings['is_active']),
|
||||
'last_test_at' => trim((string) ($settings['last_test_at'] ?? '')),
|
||||
'configure_url' => '/settings/integrations/fakturownia',
|
||||
'configure_label' => $this->translator->get('settings.integrations_hub.actions.configure'),
|
||||
];
|
||||
|
||||
@@ -26,7 +26,7 @@ final class InvoiceConfigController
|
||||
public function index(Request $request): Response
|
||||
{
|
||||
$configs = $this->repository->listAll();
|
||||
$accounts = $this->fakturownia->findAll();
|
||||
$settings = $this->fakturownia->getSettings();
|
||||
|
||||
$html = $this->template->render('settings/accounting-invoices', [
|
||||
'title' => 'Konfiguracje faktur',
|
||||
@@ -35,7 +35,7 @@ final class InvoiceConfigController
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'configs' => $configs,
|
||||
'fakturowniaAccounts' => $accounts,
|
||||
'fakturowniaSettings' => $settings,
|
||||
'successMessage' => (string) Flash::get('accounting.invoices.save', ''),
|
||||
'errorMessage' => (string) Flash::get('accounting.invoices.error', ''),
|
||||
], 'layouts/app');
|
||||
@@ -53,10 +53,7 @@ final class InvoiceConfigController
|
||||
return Response::redirect('/settings/accounting/invoices');
|
||||
}
|
||||
|
||||
$accounts = array_values(array_filter(
|
||||
$this->fakturownia->findAll(),
|
||||
static fn (array $row) => !empty($row['is_active'])
|
||||
));
|
||||
$settings = $this->fakturownia->getSettings();
|
||||
|
||||
$html = $this->template->render('settings/accounting-invoice-edit', [
|
||||
'title' => $config === null ? 'Nowa konfiguracja faktury' : 'Edycja konfiguracji faktury',
|
||||
@@ -65,7 +62,7 @@ final class InvoiceConfigController
|
||||
'user' => $this->auth->user(),
|
||||
'csrfToken' => Csrf::token(),
|
||||
'config' => $config,
|
||||
'fakturowniaAccounts' => $accounts,
|
||||
'fakturowniaSettings' => $settings,
|
||||
'successMessage' => (string) Flash::get('accounting.invoices.save', ''),
|
||||
'errorMessage' => (string) Flash::get('accounting.invoices.error', ''),
|
||||
], 'layouts/app');
|
||||
@@ -96,7 +93,7 @@ final class InvoiceConfigController
|
||||
'payment_to_days' => (int) $request->input('payment_to_days', 7),
|
||||
'default_kind' => (string) $request->input('default_kind', 'vat'),
|
||||
'is_delegated' => $request->input('is_delegated', ''),
|
||||
'integration_id' => $request->input('integration_id', ''),
|
||||
'integration_id' => $this->fakturownia->getIntegrationId(),
|
||||
'is_active' => $request->input('is_active', ''),
|
||||
]);
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ final class InvoiceConfigRepository
|
||||
{
|
||||
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;
|
||||
|
||||
if ($isDelegated === 1) {
|
||||
if ($integrationId === null || $integrationId <= 0) {
|
||||
$integrationId = $this->globalFakturowniaIntegrationId();
|
||||
if ($integrationId <= 0) {
|
||||
throw new IntegrationConfigException(
|
||||
'Przy delegacji wystawiania do Fakturowni musisz wskazac konto Fakturowni.'
|
||||
'Przed delegacja faktur skonfiguruj globalna integracje Fakturownia.'
|
||||
);
|
||||
}
|
||||
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
|
||||
{
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user