update
This commit is contained in:
@@ -2,5 +2,5 @@ projectKey=orderPRO
|
|||||||
serverUrl=https://sonar.project-pro.pl
|
serverUrl=https://sonar.project-pro.pl
|
||||||
serverVersion=26.3.0.120487
|
serverVersion=26.3.0.120487
|
||||||
dashboardUrl=https://sonar.project-pro.pl/dashboard?id=orderPRO
|
dashboardUrl=https://sonar.project-pro.pl/dashboard?id=orderPRO
|
||||||
ceTaskId=ed32f370-62ab-459a-a255-d1d5b77526b2
|
ceTaskId=37b32633-2562-4240-8b42-c6c993262727
|
||||||
ceTaskUrl=https://sonar.project-pro.pl/api/ce/task?id=ed32f370-62ab-459a-a255-d1d5b77526b2
|
ceTaskUrl=https://sonar.project-pro.pl/api/ce/task?id=37b32633-2562-4240-8b42-c6c993262727
|
||||||
|
|||||||
94
.vscode/ftp-kr.sync.cache.json
vendored
94
.vscode/ftp-kr.sync.cache.json
vendored
@@ -908,6 +908,12 @@
|
|||||||
"size": 278,
|
"size": 278,
|
||||||
"lmtime": 1774293990601,
|
"lmtime": 1774293990601,
|
||||||
"modified": false
|
"modified": false
|
||||||
|
},
|
||||||
|
"20260323_000070_create_delivery_status_mappings_table.sql": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 538,
|
||||||
|
"lmtime": 1774304095531,
|
||||||
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"seeders": {},
|
"seeders": {},
|
||||||
@@ -1024,6 +1030,18 @@
|
|||||||
"lmtime": 1774296556289,
|
"lmtime": 1774296556289,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
|
"fix_delivery_status.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 356,
|
||||||
|
"lmtime": 1774302691828,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"fix_interval.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 486,
|
||||||
|
"lmtime": 1774302992501,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
".gitignore": {
|
".gitignore": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 82,
|
"size": 82,
|
||||||
@@ -2232,8 +2250,8 @@
|
|||||||
"css": {
|
"css": {
|
||||||
"app.css": {
|
"app.css": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 53175,
|
"size": 53388,
|
||||||
"lmtime": 1774295150587,
|
"lmtime": 1774304255719,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"app.css.map": {
|
"app.css.map": {
|
||||||
@@ -2335,8 +2353,8 @@
|
|||||||
},
|
},
|
||||||
"app.scss": {
|
"app.scss": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 42669,
|
"size": 42711,
|
||||||
"lmtime": 1774295127572,
|
"lmtime": 1774304207044,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"login.css": {
|
"login.css": {
|
||||||
@@ -2364,6 +2382,12 @@
|
|||||||
"lmtime": 1773789611848,
|
"lmtime": 1773789611848,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
|
"_delivery-status.scss": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 802,
|
||||||
|
"lmtime": 1774294985916,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
"_email-send.scss": {
|
"_email-send.scss": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 2093,
|
"size": 2093,
|
||||||
@@ -2382,10 +2406,10 @@
|
|||||||
"lmtime": 1774219643850,
|
"lmtime": 1774219643850,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"_delivery-status.scss": {
|
"_delivery-status-mappings.scss": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 802,
|
"size": 212,
|
||||||
"lmtime": 1774294985916,
|
"lmtime": 1774304202952,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2433,8 +2457,8 @@
|
|||||||
"layouts": {
|
"layouts": {
|
||||||
"app.php": {
|
"app.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 9146,
|
"size": 9591,
|
||||||
"lmtime": 1773789621995,
|
"lmtime": 1774304236567,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"auth.php": {
|
"auth.php": {
|
||||||
@@ -2552,8 +2576,8 @@
|
|||||||
},
|
},
|
||||||
"cron.php": {
|
"cron.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 7941,
|
"size": 10176,
|
||||||
"lmtime": 1772992363118,
|
"lmtime": 1774302948459,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"database.php": {
|
"database.php": {
|
||||||
@@ -2562,6 +2586,12 @@
|
|||||||
"lmtime": 1772491513567,
|
"lmtime": 1772491513567,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
|
"delivery-status-mappings.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 6621,
|
||||||
|
"lmtime": 1774305162612,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
"email-mailboxes.php": {
|
"email-mailboxes.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 9893,
|
"size": 9893,
|
||||||
@@ -2598,6 +2628,12 @@
|
|||||||
"lmtime": 0,
|
"lmtime": 0,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
|
"printing.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 8683,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
"products.php": {
|
"products.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 2225,
|
"size": 2225,
|
||||||
@@ -2652,8 +2688,8 @@
|
|||||||
"routes": {
|
"routes": {
|
||||||
"web.php": {
|
"web.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 23637,
|
"size": 24661,
|
||||||
"lmtime": 1774285893715,
|
"lmtime": 1774305148776,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2693,8 +2729,8 @@
|
|||||||
},
|
},
|
||||||
"project.yml": {
|
"project.yml": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 9068,
|
"size": 9498,
|
||||||
"lmtime": 1774285628214,
|
"lmtime": 1774301665208,
|
||||||
"modified": false
|
"modified": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2937,8 +2973,8 @@
|
|||||||
},
|
},
|
||||||
"CronRepository.php": {
|
"CronRepository.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 15418,
|
"size": 16263,
|
||||||
"lmtime": 1772992332908,
|
"lmtime": 1774302741477,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"CronRunner.php": {
|
"CronRunner.php": {
|
||||||
@@ -3281,8 +3317,14 @@
|
|||||||
},
|
},
|
||||||
"CronSettingsController.php": {
|
"CronSettingsController.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 4161,
|
"size": 4710,
|
||||||
"lmtime": 1772992347512,
|
"lmtime": 1774302756544,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"DeliveryStatusMappingController.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 8524,
|
||||||
|
"lmtime": 1774305143170,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"EmailMailboxController.php": {
|
"EmailMailboxController.php": {
|
||||||
@@ -3469,8 +3511,8 @@
|
|||||||
},
|
},
|
||||||
"ApaczkaShipmentService.php": {
|
"ApaczkaShipmentService.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 33310,
|
"size": 33757,
|
||||||
"lmtime": 1774296789152,
|
"lmtime": 1774303090337,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"ApaczkaTrackingService.php": {
|
"ApaczkaTrackingService.php": {
|
||||||
@@ -3479,10 +3521,16 @@
|
|||||||
"lmtime": 1774294136946,
|
"lmtime": 1774294136946,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
|
"DeliveryStatusMappingRepository.php": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 4075,
|
||||||
|
"lmtime": 1774305131993,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
"DeliveryStatus.php": {
|
"DeliveryStatus.php": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 10246,
|
"size": 13599,
|
||||||
"lmtime": 1774296842023,
|
"lmtime": 1774304126768,
|
||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"InpostShipmentService.php": {
|
"InpostShipmentService.php": {
|
||||||
|
|||||||
@@ -6,6 +6,12 @@
|
|||||||
- Kolory akcji UI (przyciski `btn--primary` i warianty `btn--outline-primary`) sa odseparowane od koloru naglowkow (`section-title`) przez dedykowane tokeny `--c-action-primary` i `--c-action-primary-dark` w `resources/scss/shared/_ui-components.scss`.
|
- Kolory akcji UI (przyciski `btn--primary` i warianty `btn--outline-primary`) sa odseparowane od koloru naglowkow (`section-title`) przez dedykowane tokeny `--c-action-primary` i `--c-action-primary-dark` w `resources/scss/shared/_ui-components.scss`.
|
||||||
- Import Allegro zapisuje log `import` z kontekstem triggera (`manual_import`, `orders_sync`, `status_sync`) i deduplikuje powtarzalne wpisy bez realnej zmiany.
|
- Import Allegro zapisuje log `import` z kontekstem triggera (`manual_import`, `orders_sync`, `status_sync`) i deduplikuje powtarzalne wpisy bez realnej zmiany.
|
||||||
- Automatyzacja obsluguje zdarzenie `shipment.status_changed` i warunek `shipment_status` oparty o statusy biznesowe.
|
- Automatyzacja obsluguje zdarzenie `shipment.status_changed` i warunek `shipment_status` oparty o statusy biznesowe.
|
||||||
|
- Automatyzacja obsluguje akcje `issue_receipt` (Wystaw paragon) z parametrami: `receipt_config_id`, `issue_date_mode`, `duplicate_policy`.
|
||||||
|
- Orkiestracja automatyzacji obsluguje chain events: akcja moze emitowac kolejne zdarzenie (`emitEvent`), a engine propaguje wspolny kontekst lancucha.
|
||||||
|
- Zabezpieczenia chain automation (dla obecnych i przyszlych eventow):
|
||||||
|
- limit glebokosci lancucha (`MAX_CHAIN_DEPTH`),
|
||||||
|
- deduplikacja wykonania tej samej pary `event_type + rule_id` w obrebie jednego lancucha,
|
||||||
|
- limit historii wykonan w kontekście (`MAX_CHAIN_EXECUTIONS`).
|
||||||
- `ShipmentTrackingHandler` triggeruje automatyzacje tylko po zmianie `delivery_status` i przekazuje kontekst (`package_id`, `provider`, `delivery_status`, `delivery_status_raw`, `previous_status`).
|
- `ShipmentTrackingHandler` triggeruje automatyzacje tylko po zmianie `delivery_status` i przekazuje kontekst (`package_id`, `provider`, `delivery_status`, `delivery_status_raw`, `previous_status`).
|
||||||
- Kolejka wydruku ma akcje usuwania wpisu przez route `POST /settings/printing/jobs/delete` (CSRF + `OrderProAlerts.confirm`).
|
- Kolejka wydruku ma akcje usuwania wpisu przez route `POST /settings/printing/jobs/delete` (CSRF + `OrderProAlerts.confirm`).
|
||||||
|
|
||||||
@@ -151,7 +157,7 @@
|
|||||||
- `App\Modules\Accounting\AccountingController` (index — lista paragonow, export — XLSX)
|
- `App\Modules\Accounting\AccountingController` (index — lista paragonow, export — XLSX)
|
||||||
- `App\Modules\Automation\AutomationController` (index, create, store, edit, update, destroy, toggleStatus)
|
- `App\Modules\Automation\AutomationController` (index, create, store, edit, update, destroy, toggleStatus)
|
||||||
- `App\Modules\Automation\AutomationRepository` (findAll, findById, create, update, delete, toggleActive, findActiveByEvent)
|
- `App\Modules\Automation\AutomationRepository` (findAll, findById, create, update, delete, toggleActive, findActiveByEvent)
|
||||||
- `App\Modules\Automation\AutomationService` (trigger, evaluateConditions, executeActions — watcher/executor regul automatyzacji; flow: ReceiptController::store() -> trigger('receipt.created') oraz ShipmentTrackingHandler::handle() -> trigger('shipment.status_changed', context) -> ewaluacja warunkow -> EmailSendingService::send())
|
- `App\Modules\Automation\AutomationService` (trigger, evaluateConditions, executeActions — watcher/executor regul automatyzacji; flow: ReceiptController::store() -> trigger('receipt.created') oraz ShipmentTrackingHandler::handle() -> trigger('shipment.status_changed', context) -> ewaluacja warunkow -> EmailSendingService::send() / auto issue_receipt)
|
||||||
- `App\Modules\Shipments\ShipmentProviderInterface`
|
- `App\Modules\Shipments\ShipmentProviderInterface`
|
||||||
- `App\Modules\Shipments\ShipmentProviderRegistry`
|
- `App\Modules\Shipments\ShipmentProviderRegistry`
|
||||||
- `App\Modules\Shipments\ApaczkaShipmentService`
|
- `App\Modules\Shipments\ApaczkaShipmentService`
|
||||||
|
|||||||
@@ -1,5 +1,31 @@
|
|||||||
# Tech Changelog
|
# Tech Changelog
|
||||||
|
|
||||||
|
## 2026-03-25 (Automation - new action "Wystaw paragon")
|
||||||
|
- Dodano nowy typ akcji automatyzacji: `issue_receipt` (Wystaw paragon).
|
||||||
|
- Konfiguracja akcji wymaga kompletu parametrow:
|
||||||
|
- `receipt_config_id` (aktywna konfiguracja paragonu),
|
||||||
|
- `issue_date_mode` (`today` / `order_date` / `payment_date`),
|
||||||
|
- `duplicate_policy` (`skip_if_exists` / `allow_duplicates`).
|
||||||
|
- `AutomationController`:
|
||||||
|
- rozszerzono `ALLOWED_ACTION_TYPES`,
|
||||||
|
- dodano walidacje i parsowanie configu akcji `issue_receipt`,
|
||||||
|
- formularz dostaje aktywne konfiguracje paragonow i slowniki opcji.
|
||||||
|
- `resources/views/automation/form.php` i `public/assets/js/modules/automation-form.js`:
|
||||||
|
- nowa pozycja akcji `Wystaw paragon`,
|
||||||
|
- dynamiczne pola dla parametrow akcji.
|
||||||
|
- `AutomationService`:
|
||||||
|
- wykonuje automatyczne wystawienie paragonu przez `ReceiptRepository`,
|
||||||
|
- zapisuje activity log sukcesu/pominiecia/bledu,
|
||||||
|
- ma ochrone przed petla dla eventu `receipt.created` (akcja `issue_receipt` jest pomijana i logowana),
|
||||||
|
- obsluguje polityke duplikatow.
|
||||||
|
- Aktualizacja DI:
|
||||||
|
- `routes/web.php` i `CronHandlerFactory` przekazuja do `AutomationService` zaleznosci `ReceiptRepository` i `ReceiptConfigRepository`.
|
||||||
|
- Dodano systemowy mechanizm chain automation dla obecnych i przyszlych zdarzen:
|
||||||
|
- wspolny kontekst lancucha (`__automation_chain`) propagowany miedzy kolejnymi triggerami,
|
||||||
|
- `emitEvent(...)` jako bezpieczny mechanizm emitowania kolejnych eventow z akcji,
|
||||||
|
- ochrona anty-petla przez deduplikacje wykonania `event_type + rule_id` w jednym lancuchu,
|
||||||
|
- limit glebokosci lancucha (`MAX_CHAIN_DEPTH`) i limit historii wykonan (`MAX_CHAIN_EXECUTIONS`).
|
||||||
|
|
||||||
## 2026-03-25 (Phase 43 - Print Queue Entry Removal, Plan 01)
|
## 2026-03-25 (Phase 43 - Print Queue Entry Removal, Plan 01)
|
||||||
- Dodano usuwanie wpisu kolejki wydruku:
|
- Dodano usuwanie wpisu kolejki wydruku:
|
||||||
- `PrintJobRepository::deleteById(int): bool`,
|
- `PrintJobRepository::deleteById(int): bool`,
|
||||||
|
|||||||
@@ -66,6 +66,35 @@
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildIssueReceiptActionConfig(namePrefix) {
|
||||||
|
var html = '<select class="form-control" name="' + namePrefix + '[receipt_config_id]">'
|
||||||
|
+ '<option value="">-- Wybierz konfiguracje paragonu --</option>';
|
||||||
|
(data.receiptConfigs || []).forEach(function(cfg) {
|
||||||
|
html += '<option value="' + cfg.id + '">'
|
||||||
|
+ escapeHtml(cfg.name) + ' (' + escapeHtml(cfg.number_format || '') + ')'
|
||||||
|
+ '</option>';
|
||||||
|
});
|
||||||
|
html += '</select>';
|
||||||
|
|
||||||
|
html += '<select class="form-control" name="' + namePrefix + '[issue_date_mode]">';
|
||||||
|
(data.receiptIssueDateModes || []).forEach(function(mode) {
|
||||||
|
var labels = data.receiptIssueDateModeLabels || {};
|
||||||
|
var label = labels[mode] || mode;
|
||||||
|
html += '<option value="' + escapeHtml(mode) + '">' + escapeHtml(label) + '</option>';
|
||||||
|
});
|
||||||
|
html += '</select>';
|
||||||
|
|
||||||
|
html += '<select class="form-control" name="' + namePrefix + '[duplicate_policy]">';
|
||||||
|
(data.receiptDuplicatePolicies || []).forEach(function(policy) {
|
||||||
|
var labels = data.receiptDuplicatePolicyLabels || {};
|
||||||
|
var label = labels[policy] || policy;
|
||||||
|
html += '<option value="' + escapeHtml(policy) + '">' + escapeHtml(label) + '</option>';
|
||||||
|
});
|
||||||
|
html += '</select>';
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
function addCondition() {
|
function addCondition() {
|
||||||
var idx = getNextIndex(conditionsContainer);
|
var idx = getNextIndex(conditionsContainer);
|
||||||
var namePrefix = 'conditions[' + idx + ']';
|
var namePrefix = 'conditions[' + idx + ']';
|
||||||
@@ -99,6 +128,7 @@
|
|||||||
row.innerHTML = '<div class="automation-row__fields">'
|
row.innerHTML = '<div class="automation-row__fields">'
|
||||||
+ '<select class="form-control automation-row__type" name="' + namePrefix + '[type]" onchange="window.AutomationForm.onActionTypeChange(this)">'
|
+ '<select class="form-control automation-row__type" name="' + namePrefix + '[type]" onchange="window.AutomationForm.onActionTypeChange(this)">'
|
||||||
+ '<option value="send_email" selected>Wyslij e-mail</option>'
|
+ '<option value="send_email" selected>Wyslij e-mail</option>'
|
||||||
|
+ '<option value="issue_receipt">Wystaw paragon</option>'
|
||||||
+ '</select>'
|
+ '</select>'
|
||||||
+ '<div class="automation-row__config">'
|
+ '<div class="automation-row__config">'
|
||||||
+ buildEmailActionConfig(namePrefix)
|
+ buildEmailActionConfig(namePrefix)
|
||||||
@@ -137,6 +167,10 @@
|
|||||||
|
|
||||||
if (select.value === 'send_email') {
|
if (select.value === 'send_email') {
|
||||||
configDiv.innerHTML = buildEmailActionConfig(namePrefix);
|
configDiv.innerHTML = buildEmailActionConfig(namePrefix);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (select.value === 'issue_receipt') {
|
||||||
|
configDiv.innerHTML = buildIssueReceiptActionConfig(namePrefix);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,18 @@ $recipientLabels = [
|
|||||||
'client_and_company' => 'Klient + e-mail z danych firmy',
|
'client_and_company' => 'Klient + e-mail z danych firmy',
|
||||||
'company' => 'E-mail z danych firmy',
|
'company' => 'E-mail z danych firmy',
|
||||||
];
|
];
|
||||||
|
$receiptConfigs = is_array($receiptConfigs ?? null) ? $receiptConfigs : [];
|
||||||
|
$receiptIssueDateModes = is_array($receiptIssueDateModes ?? null) ? $receiptIssueDateModes : [];
|
||||||
|
$receiptDuplicatePolicies = is_array($receiptDuplicatePolicies ?? null) ? $receiptDuplicatePolicies : [];
|
||||||
|
$receiptIssueDateModeLabels = [
|
||||||
|
'today' => 'Data dzisiejsza',
|
||||||
|
'order_date' => 'Data zamowienia',
|
||||||
|
'payment_date' => 'Data platnosci (fallback: dzisiaj)',
|
||||||
|
];
|
||||||
|
$receiptDuplicatePolicyLabels = [
|
||||||
|
'skip_if_exists' => 'Pomin jesli paragon juz istnieje',
|
||||||
|
'allow_duplicates' => 'Wystawiaj kolejne paragony',
|
||||||
|
];
|
||||||
$shipmentStatusOptions = is_array($shipmentStatusOptions ?? null) ? $shipmentStatusOptions : [];
|
$shipmentStatusOptions = is_array($shipmentStatusOptions ?? null) ? $shipmentStatusOptions : [];
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@@ -112,20 +124,51 @@ $shipmentStatusOptions = is_array($shipmentStatusOptions ?? null) ? $shipmentSta
|
|||||||
<div class="automation-row__fields">
|
<div class="automation-row__fields">
|
||||||
<select class="form-control automation-row__type" name="actions[<?= $idx ?>][type]" onchange="window.AutomationForm.onActionTypeChange(this)">
|
<select class="form-control automation-row__type" name="actions[<?= $idx ?>][type]" onchange="window.AutomationForm.onActionTypeChange(this)">
|
||||||
<option value="send_email"<?= ((string) ($act['action_type'] ?? '')) === 'send_email' ? ' selected' : '' ?>>Wyslij e-mail</option>
|
<option value="send_email"<?= ((string) ($act['action_type'] ?? '')) === 'send_email' ? ' selected' : '' ?>>Wyslij e-mail</option>
|
||||||
|
<option value="issue_receipt"<?= ((string) ($act['action_type'] ?? '')) === 'issue_receipt' ? ' selected' : '' ?>>Wystaw paragon</option>
|
||||||
</select>
|
</select>
|
||||||
<div class="automation-row__config">
|
<div class="automation-row__config">
|
||||||
<?php $actConfig = is_array($act['action_config'] ?? null) ? $act['action_config'] : []; ?>
|
<?php
|
||||||
<select class="form-control" name="actions[<?= $idx ?>][template_id]">
|
$actConfig = is_array($act['action_config'] ?? null) ? $act['action_config'] : [];
|
||||||
<option value="">-- Wybierz szablon --</option>
|
$actionType = (string) ($act['action_type'] ?? 'send_email');
|
||||||
<?php foreach ($emailTemplates as $tpl): ?>
|
?>
|
||||||
<option value="<?= (int) $tpl['id'] ?>"<?= ((int) ($actConfig['template_id'] ?? 0)) === (int) $tpl['id'] ? ' selected' : '' ?>><?= $e((string) ($tpl['name'] ?? '')) ?></option>
|
<?php if ($actionType === 'issue_receipt'): ?>
|
||||||
<?php endforeach; ?>
|
<select class="form-control" name="actions[<?= $idx ?>][receipt_config_id]">
|
||||||
</select>
|
<option value="">-- Wybierz konfiguracje paragonu --</option>
|
||||||
<select class="form-control" name="actions[<?= $idx ?>][recipient]">
|
<?php foreach ($receiptConfigs as $cfg): ?>
|
||||||
<?php foreach ($recipientLabels as $key => $label): ?>
|
<option value="<?= (int) ($cfg['id'] ?? 0) ?>"<?= ((int) ($actConfig['receipt_config_id'] ?? 0)) === (int) ($cfg['id'] ?? 0) ? ' selected' : '' ?>>
|
||||||
<option value="<?= $e($key) ?>"<?= ((string) ($actConfig['recipient'] ?? '')) === $key ? ' selected' : '' ?>><?= $e($label) ?></option>
|
<?= $e((string) ($cfg['name'] ?? '')) ?> (<?= $e((string) ($cfg['number_format'] ?? '')) ?>)
|
||||||
<?php endforeach; ?>
|
</option>
|
||||||
</select>
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<select class="form-control" name="actions[<?= $idx ?>][issue_date_mode]">
|
||||||
|
<?php foreach ($receiptIssueDateModes as $mode): ?>
|
||||||
|
<?php $modeKey = (string) $mode; ?>
|
||||||
|
<option value="<?= $e($modeKey) ?>"<?= ((string) ($actConfig['issue_date_mode'] ?? 'today')) === $modeKey ? ' selected' : '' ?>>
|
||||||
|
<?= $e((string) ($receiptIssueDateModeLabels[$modeKey] ?? $modeKey)) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<select class="form-control" name="actions[<?= $idx ?>][duplicate_policy]">
|
||||||
|
<?php foreach ($receiptDuplicatePolicies as $policy): ?>
|
||||||
|
<?php $policyKey = (string) $policy; ?>
|
||||||
|
<option value="<?= $e($policyKey) ?>"<?= ((string) ($actConfig['duplicate_policy'] ?? 'skip_if_exists')) === $policyKey ? ' selected' : '' ?>>
|
||||||
|
<?= $e((string) ($receiptDuplicatePolicyLabels[$policyKey] ?? $policyKey)) ?>
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php else: ?>
|
||||||
|
<select class="form-control" name="actions[<?= $idx ?>][template_id]">
|
||||||
|
<option value="">-- Wybierz szablon --</option>
|
||||||
|
<?php foreach ($emailTemplates as $tpl): ?>
|
||||||
|
<option value="<?= (int) $tpl['id'] ?>"<?= ((int) ($actConfig['template_id'] ?? 0)) === (int) $tpl['id'] ? ' selected' : '' ?>><?= $e((string) ($tpl['name'] ?? '')) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<select class="form-control" name="actions[<?= $idx ?>][recipient]">
|
||||||
|
<?php foreach ($recipientLabels as $key => $label): ?>
|
||||||
|
<option value="<?= $e($key) ?>"<?= ((string) ($actConfig['recipient'] ?? '')) === $key ? ' selected' : '' ?>><?= $e($label) ?></option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn--sm btn--danger automation-row__remove" onclick="window.AutomationForm.removeRow(this)">×</button>
|
<button type="button" class="btn btn--sm btn--danger automation-row__remove" onclick="window.AutomationForm.removeRow(this)">×</button>
|
||||||
@@ -150,7 +193,18 @@ window.AutomationFormData = {
|
|||||||
emailTemplates: <?= json_encode(array_map(function($t) {
|
emailTemplates: <?= json_encode(array_map(function($t) {
|
||||||
return ['id' => (int) $t['id'], 'name' => (string) ($t['name'] ?? '')];
|
return ['id' => (int) $t['id'], 'name' => (string) ($t['name'] ?? '')];
|
||||||
}, $emailTemplates), JSON_UNESCAPED_UNICODE) ?>,
|
}, $emailTemplates), JSON_UNESCAPED_UNICODE) ?>,
|
||||||
|
receiptConfigs: <?= json_encode(array_map(function($cfg) {
|
||||||
|
return [
|
||||||
|
'id' => (int) ($cfg['id'] ?? 0),
|
||||||
|
'name' => (string) ($cfg['name'] ?? ''),
|
||||||
|
'number_format' => (string) ($cfg['number_format'] ?? '')
|
||||||
|
];
|
||||||
|
}, $receiptConfigs), JSON_UNESCAPED_UNICODE) ?>,
|
||||||
recipientLabels: <?= json_encode($recipientLabels, JSON_UNESCAPED_UNICODE) ?>,
|
recipientLabels: <?= json_encode($recipientLabels, JSON_UNESCAPED_UNICODE) ?>,
|
||||||
|
receiptIssueDateModes: <?= json_encode($receiptIssueDateModes, JSON_UNESCAPED_UNICODE) ?>,
|
||||||
|
receiptIssueDateModeLabels: <?= json_encode($receiptIssueDateModeLabels, JSON_UNESCAPED_UNICODE) ?>,
|
||||||
|
receiptDuplicatePolicies: <?= json_encode($receiptDuplicatePolicies, JSON_UNESCAPED_UNICODE) ?>,
|
||||||
|
receiptDuplicatePolicyLabels: <?= json_encode($receiptDuplicatePolicyLabels, JSON_UNESCAPED_UNICODE) ?>,
|
||||||
shipmentStatusOptions: <?= json_encode($shipmentStatusOptions, JSON_UNESCAPED_UNICODE) ?>
|
shipmentStatusOptions: <?= json_encode($shipmentStatusOptions, JSON_UNESCAPED_UNICODE) ?>
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -59,9 +59,9 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
|
|||||||
<td><?= $e((string) ($key['last_used_at'] ?? '-')) ?></td>
|
<td><?= $e((string) ($key['last_used_at'] ?? '-')) ?></td>
|
||||||
<td><?= $e((string) ($key['created_at'] ?? '')) ?></td>
|
<td><?= $e((string) ($key['created_at'] ?? '')) ?></td>
|
||||||
<td>
|
<td>
|
||||||
<form method="post" action="/settings/printing/keys/<?= $e((string) ($key['id'] ?? '')) ?>/delete" style="display:inline">
|
<form method="post" action="/settings/printing/keys/<?= $e((string) ($key['id'] ?? '')) ?>/delete" style="display:inline" class="js-delete-api-key-form">
|
||||||
<input type="hidden" name="_token" value="<?= $e((string) ($csrfToken ?? '')) ?>">
|
<input type="hidden" name="_token" value="<?= $e((string) ($csrfToken ?? '')) ?>">
|
||||||
<button type="button" class="btn btn--danger btn--sm" onclick="window.OrderProAlerts.confirm('Czy na pewno chcesz usunac ten klucz API?', function() { this.closest('form').submit(); }.bind(this))">Usun</button>
|
<button type="button" class="btn btn--danger btn--sm js-delete-api-key-btn">Usun</button>
|
||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -177,11 +177,41 @@ $currentStatusFilter = (string) ($printStatusFilter ?? '');
|
|||||||
var form = btn.closest('form');
|
var form = btn.closest('form');
|
||||||
if (!form) return;
|
if (!form) return;
|
||||||
if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
|
if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
|
||||||
window.OrderProAlerts.confirm(
|
window.OrderProAlerts.confirm({
|
||||||
'Usuwanie wpisu z kolejki',
|
title: 'Usuwanie wpisu z kolejki',
|
||||||
'Czy na pewno chcesz usunac ten wpis kolejki wydruku?',
|
message: 'Czy na pewno chcesz usunac ten wpis kolejki wydruku?',
|
||||||
function () { form.submit(); }
|
confirmLabel: 'Usun',
|
||||||
);
|
cancelLabel: 'Anuluj',
|
||||||
|
danger: true
|
||||||
|
}).then(function (confirmed) {
|
||||||
|
if (confirmed) {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelectorAll('.js-delete-api-key-btn').forEach(function (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var form = btn.closest('form');
|
||||||
|
if (!form) return;
|
||||||
|
if (window.OrderProAlerts && window.OrderProAlerts.confirm) {
|
||||||
|
window.OrderProAlerts.confirm({
|
||||||
|
title: 'Usuwanie klucza API',
|
||||||
|
message: 'Czy na pewno chcesz usunac ten klucz API?',
|
||||||
|
confirmLabel: 'Usun',
|
||||||
|
cancelLabel: 'Anuluj',
|
||||||
|
danger: true
|
||||||
|
}).then(function (confirmed) {
|
||||||
|
if (confirmed) {
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
form.submit();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -233,7 +233,8 @@ return static function (Application $app): void {
|
|||||||
$template,
|
$template,
|
||||||
$translator,
|
$translator,
|
||||||
$auth,
|
$auth,
|
||||||
$automationRepository
|
$automationRepository,
|
||||||
|
$receiptConfigRepository
|
||||||
);
|
);
|
||||||
$variableResolver = new VariableResolver();
|
$variableResolver = new VariableResolver();
|
||||||
$attachmentGenerator = new AttachmentGenerator($receiptRepository, $receiptConfigRepository, $template);
|
$attachmentGenerator = new AttachmentGenerator($receiptRepository, $receiptConfigRepository, $template);
|
||||||
@@ -249,7 +250,9 @@ return static function (Application $app): void {
|
|||||||
$automationRepository,
|
$automationRepository,
|
||||||
$emailSendingService,
|
$emailSendingService,
|
||||||
new OrdersRepository($app->db()),
|
new OrdersRepository($app->db()),
|
||||||
$companySettingsRepository
|
$companySettingsRepository,
|
||||||
|
$receiptRepository,
|
||||||
|
$receiptConfigRepository
|
||||||
);
|
);
|
||||||
$printJobRepository = new PrintJobRepository($app->db());
|
$printJobRepository = new PrintJobRepository($app->db());
|
||||||
$ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders, $receiptRepository, $receiptConfigRepository, $emailSendingService, $emailTemplateRepository, $emailMailboxRepository, $app->basePath('storage'), $printJobRepository);
|
$ordersController = new OrdersController($template, $translator, $auth, $app->orders(), $shipmentPackageRepositoryForOrders, $receiptRepository, $receiptConfigRepository, $emailSendingService, $emailTemplateRepository, $emailMailboxRepository, $app->basePath('storage'), $printJobRepository);
|
||||||
|
|||||||
@@ -10,14 +10,17 @@ use App\Core\Security\Csrf;
|
|||||||
use App\Core\Support\Flash;
|
use App\Core\Support\Flash;
|
||||||
use App\Core\View\Template;
|
use App\Core\View\Template;
|
||||||
use App\Modules\Auth\AuthService;
|
use App\Modules\Auth\AuthService;
|
||||||
|
use App\Modules\Settings\ReceiptConfigRepository;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
final class AutomationController
|
final class AutomationController
|
||||||
{
|
{
|
||||||
private const ALLOWED_EVENTS = ['receipt.created', 'shipment.status_changed'];
|
private const ALLOWED_EVENTS = ['receipt.created', 'shipment.status_changed'];
|
||||||
private const ALLOWED_CONDITION_TYPES = ['integration', 'shipment_status'];
|
private const ALLOWED_CONDITION_TYPES = ['integration', 'shipment_status'];
|
||||||
private const ALLOWED_ACTION_TYPES = ['send_email'];
|
private const ALLOWED_ACTION_TYPES = ['send_email', 'issue_receipt'];
|
||||||
private const ALLOWED_RECIPIENTS = ['client', 'client_and_company', 'company'];
|
private const ALLOWED_RECIPIENTS = ['client', 'client_and_company', 'company'];
|
||||||
|
private const ALLOWED_RECEIPT_ISSUE_DATE_MODES = ['today', 'order_date', 'payment_date'];
|
||||||
|
private const ALLOWED_RECEIPT_DUPLICATE_POLICIES = ['skip_if_exists', 'allow_duplicates'];
|
||||||
private const SHIPMENT_STATUS_OPTIONS = [
|
private const SHIPMENT_STATUS_OPTIONS = [
|
||||||
'registered' => ['label' => 'Przesylka zarejestrowana', 'statuses' => ['created', 'confirmed']],
|
'registered' => ['label' => 'Przesylka zarejestrowana', 'statuses' => ['created', 'confirmed']],
|
||||||
'ready_for_pickup' => ['label' => 'Przesylka do odbioru', 'statuses' => ['ready_for_pickup']],
|
'ready_for_pickup' => ['label' => 'Przesylka do odbioru', 'statuses' => ['ready_for_pickup']],
|
||||||
@@ -32,7 +35,8 @@ final class AutomationController
|
|||||||
private readonly Template $template,
|
private readonly Template $template,
|
||||||
private readonly Translator $translator,
|
private readonly Translator $translator,
|
||||||
private readonly AuthService $auth,
|
private readonly AuthService $auth,
|
||||||
private readonly AutomationRepository $repository
|
private readonly AutomationRepository $repository,
|
||||||
|
private readonly ReceiptConfigRepository $receiptConfigs
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -194,6 +198,9 @@ final class AutomationController
|
|||||||
'conditionTypes' => self::ALLOWED_CONDITION_TYPES,
|
'conditionTypes' => self::ALLOWED_CONDITION_TYPES,
|
||||||
'actionTypes' => self::ALLOWED_ACTION_TYPES,
|
'actionTypes' => self::ALLOWED_ACTION_TYPES,
|
||||||
'recipientOptions' => self::ALLOWED_RECIPIENTS,
|
'recipientOptions' => self::ALLOWED_RECIPIENTS,
|
||||||
|
'receiptConfigs' => $this->listActiveReceiptConfigs(),
|
||||||
|
'receiptIssueDateModes' => self::ALLOWED_RECEIPT_ISSUE_DATE_MODES,
|
||||||
|
'receiptDuplicatePolicies' => self::ALLOWED_RECEIPT_DUPLICATE_POLICIES,
|
||||||
'shipmentStatusOptions' => self::SHIPMENT_STATUS_OPTIONS,
|
'shipmentStatusOptions' => self::SHIPMENT_STATUS_OPTIONS,
|
||||||
'errorMessage' => Flash::get('settings.automation.error', ''),
|
'errorMessage' => Flash::get('settings.automation.error', ''),
|
||||||
], 'layouts/app');
|
], 'layouts/app');
|
||||||
@@ -366,6 +373,51 @@ final class AutomationController
|
|||||||
return ['template_id' => $templateId, 'recipient' => $recipient];
|
return ['template_id' => $templateId, 'recipient' => $recipient];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($type === 'issue_receipt') {
|
||||||
|
$configId = (int) ($action['receipt_config_id'] ?? 0);
|
||||||
|
$issueDateMode = (string) ($action['issue_date_mode'] ?? '');
|
||||||
|
$duplicatePolicy = (string) ($action['duplicate_policy'] ?? '');
|
||||||
|
|
||||||
|
if (
|
||||||
|
$configId <= 0
|
||||||
|
|| !in_array($issueDateMode, self::ALLOWED_RECEIPT_ISSUE_DATE_MODES, true)
|
||||||
|
|| !in_array($duplicatePolicy, self::ALLOWED_RECEIPT_DUPLICATE_POLICIES, true)
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'receipt_config_id' => $configId,
|
||||||
|
'issue_date_mode' => $issueDateMode,
|
||||||
|
'duplicate_policy' => $duplicatePolicy,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<array{id:int,name:string,number_format:string}>
|
||||||
|
*/
|
||||||
|
private function listActiveReceiptConfigs(): array
|
||||||
|
{
|
||||||
|
$all = $this->receiptConfigs->listAll();
|
||||||
|
$result = [];
|
||||||
|
foreach ($all as $config) {
|
||||||
|
if ((int) ($config['is_active'] ?? 0) !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$configId = (int) ($config['id'] ?? 0);
|
||||||
|
if ($configId <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$result[] = [
|
||||||
|
'id' => $configId,
|
||||||
|
'name' => (string) ($config['name'] ?? ''),
|
||||||
|
'number_format' => (string) ($config['number_format'] ?? ''),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,13 +3,19 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Modules\Automation;
|
namespace App\Modules\Automation;
|
||||||
|
|
||||||
|
use App\Modules\Accounting\ReceiptRepository;
|
||||||
use App\Modules\Email\EmailSendingService;
|
use App\Modules\Email\EmailSendingService;
|
||||||
use App\Modules\Orders\OrdersRepository;
|
use App\Modules\Orders\OrdersRepository;
|
||||||
use App\Modules\Settings\CompanySettingsRepository;
|
use App\Modules\Settings\CompanySettingsRepository;
|
||||||
|
use App\Modules\Settings\ReceiptConfigRepository;
|
||||||
use Throwable;
|
use Throwable;
|
||||||
|
|
||||||
final class AutomationService
|
final class AutomationService
|
||||||
{
|
{
|
||||||
|
private const CHAIN_STATE_KEY = '__automation_chain';
|
||||||
|
private const MAX_CHAIN_DEPTH = 8;
|
||||||
|
private const MAX_CHAIN_EXECUTIONS = 200;
|
||||||
|
|
||||||
private const SHIPMENT_STATUS_OPTION_MAP = [
|
private const SHIPMENT_STATUS_OPTION_MAP = [
|
||||||
'registered' => ['created', 'confirmed'],
|
'registered' => ['created', 'confirmed'],
|
||||||
'ready_for_pickup' => ['ready_for_pickup'],
|
'ready_for_pickup' => ['ready_for_pickup'],
|
||||||
@@ -24,7 +30,9 @@ final class AutomationService
|
|||||||
private readonly AutomationRepository $repository,
|
private readonly AutomationRepository $repository,
|
||||||
private readonly EmailSendingService $emailService,
|
private readonly EmailSendingService $emailService,
|
||||||
private readonly OrdersRepository $orders,
|
private readonly OrdersRepository $orders,
|
||||||
private readonly CompanySettingsRepository $companySettings
|
private readonly CompanySettingsRepository $companySettings,
|
||||||
|
private readonly ReceiptRepository $receipts,
|
||||||
|
private readonly ReceiptConfigRepository $receiptConfigs
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -33,6 +41,12 @@ final class AutomationService
|
|||||||
*/
|
*/
|
||||||
public function trigger(string $eventType, int $orderId, array $context = []): void
|
public function trigger(string $eventType, int $orderId, array $context = []): void
|
||||||
{
|
{
|
||||||
|
$chainState = $this->getChainState($context);
|
||||||
|
if ($chainState['depth'] > self::MAX_CHAIN_DEPTH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$context = $this->withChainState($context, $chainState);
|
||||||
|
|
||||||
$rules = $this->repository->findActiveByEvent($eventType);
|
$rules = $this->repository->findActiveByEvent($eventType);
|
||||||
if ($rules === []) {
|
if ($rules === []) {
|
||||||
return;
|
return;
|
||||||
@@ -47,12 +61,19 @@ final class AutomationService
|
|||||||
|
|
||||||
foreach ($rules as $rule) {
|
foreach ($rules as $rule) {
|
||||||
try {
|
try {
|
||||||
|
$ruleId = (int) ($rule['id'] ?? 0);
|
||||||
|
$executionKey = $this->buildExecutionKey($eventType, $ruleId);
|
||||||
|
if ($this->isExecutionAlreadyProcessed($chainState, $executionKey)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
$conditions = is_array($rule['conditions'] ?? null) ? $rule['conditions'] : [];
|
$conditions = is_array($rule['conditions'] ?? null) ? $rule['conditions'] : [];
|
||||||
$actions = is_array($rule['actions'] ?? null) ? $rule['actions'] : [];
|
$actions = is_array($rule['actions'] ?? null) ? $rule['actions'] : [];
|
||||||
$ruleName = (string) ($rule['name'] ?? '');
|
$ruleName = (string) ($rule['name'] ?? '');
|
||||||
|
$ruleContext = $this->withExecution($context, $executionKey);
|
||||||
|
|
||||||
if ($this->evaluateConditions($conditions, $order, $context)) {
|
if ($this->evaluateConditions($conditions, $order, $ruleContext)) {
|
||||||
$this->executeActions($actions, $orderId, $ruleName);
|
$this->executeActions($actions, $orderId, $ruleName, $ruleContext);
|
||||||
}
|
}
|
||||||
} catch (Throwable) {
|
} catch (Throwable) {
|
||||||
// Blad jednej reguly nie blokuje kolejnych
|
// Blad jednej reguly nie blokuje kolejnych
|
||||||
@@ -151,8 +172,9 @@ final class AutomationService
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param list<array<string, mixed>> $actions
|
* @param list<array<string, mixed>> $actions
|
||||||
|
* @param array<string, mixed> $context
|
||||||
*/
|
*/
|
||||||
private function executeActions(array $actions, int $orderId, string $ruleName): void
|
private function executeActions(array $actions, int $orderId, string $ruleName, array $context): void
|
||||||
{
|
{
|
||||||
foreach ($actions as $action) {
|
foreach ($actions as $action) {
|
||||||
$type = (string) ($action['action_type'] ?? '');
|
$type = (string) ($action['action_type'] ?? '');
|
||||||
@@ -160,6 +182,11 @@ final class AutomationService
|
|||||||
|
|
||||||
if ($type === 'send_email') {
|
if ($type === 'send_email') {
|
||||||
$this->handleSendEmail($config, $orderId, $ruleName);
|
$this->handleSendEmail($config, $orderId, $ruleName);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($type === 'issue_receipt') {
|
||||||
|
$this->handleIssueReceipt($config, $orderId, $ruleName, $context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -214,4 +241,396 @@ final class AutomationService
|
|||||||
$companyName
|
$companyName
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $config
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
*/
|
||||||
|
private function handleIssueReceipt(array $config, int $orderId, string $ruleName, array $context): void
|
||||||
|
{
|
||||||
|
$actorName = 'Automatyzacja: ' . $ruleName;
|
||||||
|
|
||||||
|
$configId = (int) ($config['receipt_config_id'] ?? 0);
|
||||||
|
if ($configId <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$issueDateMode = (string) ($config['issue_date_mode'] ?? 'today');
|
||||||
|
$duplicatePolicy = (string) ($config['duplicate_policy'] ?? 'skip_if_exists');
|
||||||
|
|
||||||
|
$receiptConfig = $this->receiptConfigs->findById($configId);
|
||||||
|
if ($receiptConfig === null || (int) ($receiptConfig['is_active'] ?? 0) !== 1) {
|
||||||
|
$this->orders->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
'automation_receipt_failed',
|
||||||
|
$actorName . ' - nieprawidlowa lub nieaktywna konfiguracja paragonu',
|
||||||
|
['receipt_config_id' => $configId],
|
||||||
|
'system',
|
||||||
|
$actorName
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$existingReceipts = $this->receipts->findByOrderId($orderId);
|
||||||
|
if ($duplicatePolicy === 'skip_if_exists' && $existingReceipts !== []) {
|
||||||
|
$this->orders->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
'automation_receipt_skipped',
|
||||||
|
$actorName . ' - pomieto, paragon juz istnieje dla zamowienia',
|
||||||
|
['duplicate_policy' => $duplicatePolicy, 'existing_count' => count($existingReceipts)],
|
||||||
|
'system',
|
||||||
|
$actorName
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$details = $this->orders->findDetails($orderId);
|
||||||
|
if ($details === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$order = is_array($details['order'] ?? null) ? $details['order'] : [];
|
||||||
|
$items = is_array($details['items'] ?? null) ? $details['items'] : [];
|
||||||
|
$addresses = is_array($details['addresses'] ?? null) ? $details['addresses'] : [];
|
||||||
|
$payments = is_array($details['payments'] ?? null) ? $details['payments'] : [];
|
||||||
|
|
||||||
|
$issueDate = $this->resolveIssueDate($issueDateMode, $order, $payments);
|
||||||
|
$saleDate = $this->resolveSaleDate($receiptConfig, $order, $payments, $issueDate);
|
||||||
|
$orderReference = $this->resolveOrderReference($receiptConfig, $order);
|
||||||
|
$sellerSnapshot = $this->buildSellerSnapshot();
|
||||||
|
$buyerSnapshot = $this->buildBuyerSnapshot($addresses);
|
||||||
|
['items' => $itemsSnapshot, 'total_gross' => $totalGross] = $this->buildItemsSnapshot($items);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$receiptNumber = $this->receipts->getNextNumber(
|
||||||
|
$configId,
|
||||||
|
(string) ($receiptConfig['number_format'] ?? 'PAR/%N/%M/%Y'),
|
||||||
|
(string) ($receiptConfig['numbering_type'] ?? 'monthly')
|
||||||
|
);
|
||||||
|
$this->receipts->create([
|
||||||
|
'order_id' => $orderId,
|
||||||
|
'config_id' => $configId,
|
||||||
|
'receipt_number' => $receiptNumber,
|
||||||
|
'issue_date' => $issueDate,
|
||||||
|
'sale_date' => $saleDate,
|
||||||
|
'seller_data_json' => json_encode($sellerSnapshot, JSON_UNESCAPED_UNICODE),
|
||||||
|
'buyer_data_json' => $buyerSnapshot !== null ? json_encode($buyerSnapshot, JSON_UNESCAPED_UNICODE) : null,
|
||||||
|
'items_json' => json_encode($itemsSnapshot, JSON_UNESCAPED_UNICODE),
|
||||||
|
'total_net' => number_format($totalGross, 2, '.', ''),
|
||||||
|
'total_gross' => number_format($totalGross, 2, '.', ''),
|
||||||
|
'order_reference_value' => $orderReference,
|
||||||
|
'created_by' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->orders->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
'receipt_issued',
|
||||||
|
'Wystawiono paragon: ' . $receiptNumber,
|
||||||
|
['receipt_number' => $receiptNumber, 'config_id' => $configId, 'total_gross' => number_format($totalGross, 2, '.', '')],
|
||||||
|
'system',
|
||||||
|
$actorName
|
||||||
|
);
|
||||||
|
|
||||||
|
// Chain automation: issuing receipt from one rule should trigger
|
||||||
|
// rules listening on receipt.created. Uses generic chain context
|
||||||
|
// with depth + rule deduplication to prevent loops.
|
||||||
|
$this->emitEvent(
|
||||||
|
'receipt.created',
|
||||||
|
$orderId,
|
||||||
|
$context,
|
||||||
|
[
|
||||||
|
'automation_source' => 'issue_receipt',
|
||||||
|
'automation_rule' => $ruleName,
|
||||||
|
'receipt_number' => $receiptNumber,
|
||||||
|
'receipt_config_id' => $configId,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
} catch (Throwable $exception) {
|
||||||
|
$this->orders->recordActivity(
|
||||||
|
$orderId,
|
||||||
|
'automation_receipt_failed',
|
||||||
|
$actorName . ' - blad wystawiania paragonu',
|
||||||
|
['error' => $exception->getMessage(), 'receipt_config_id' => $configId],
|
||||||
|
'system',
|
||||||
|
$actorName
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $order
|
||||||
|
* @param list<array<string, mixed>> $payments
|
||||||
|
*/
|
||||||
|
private function resolveIssueDate(string $mode, array $order, array $payments): string
|
||||||
|
{
|
||||||
|
if ($mode === 'order_date') {
|
||||||
|
$orderedAt = trim((string) ($order['ordered_at'] ?? ''));
|
||||||
|
if ($orderedAt !== '') {
|
||||||
|
$timestamp = strtotime($orderedAt);
|
||||||
|
if ($timestamp !== false) {
|
||||||
|
return date('Y-m-d', $timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($mode === 'payment_date') {
|
||||||
|
$firstPayment = $payments[0] ?? [];
|
||||||
|
$paymentDate = trim((string) ($firstPayment['payment_date'] ?? ''));
|
||||||
|
if ($paymentDate !== '') {
|
||||||
|
$timestamp = strtotime($paymentDate);
|
||||||
|
if ($timestamp !== false) {
|
||||||
|
return date('Y-m-d', $timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return date('Y-m-d');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $receiptConfig
|
||||||
|
* @param array<string, mixed> $order
|
||||||
|
* @param list<array<string, mixed>> $payments
|
||||||
|
*/
|
||||||
|
private function resolveSaleDate(array $receiptConfig, array $order, array $payments, string $issueDate): string
|
||||||
|
{
|
||||||
|
$source = (string) ($receiptConfig['sale_date_source'] ?? 'issue_date');
|
||||||
|
|
||||||
|
if ($source === 'order_date') {
|
||||||
|
$ordered = (string) ($order['ordered_at'] ?? '');
|
||||||
|
if ($ordered !== '') {
|
||||||
|
$ts = strtotime($ordered);
|
||||||
|
return $ts !== false ? date('Y-m-d', $ts) : $issueDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($source === 'payment_date' && $payments !== []) {
|
||||||
|
$lastPayment = $payments[0] ?? [];
|
||||||
|
$payDate = (string) ($lastPayment['payment_date'] ?? '');
|
||||||
|
if ($payDate !== '') {
|
||||||
|
$ts = strtotime($payDate);
|
||||||
|
return $ts !== false ? date('Y-m-d', $ts) : $issueDate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $issueDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $receiptConfig
|
||||||
|
* @param array<string, mixed> $order
|
||||||
|
*/
|
||||||
|
private function resolveOrderReference(array $receiptConfig, array $order): ?string
|
||||||
|
{
|
||||||
|
$ref = (string) ($receiptConfig['order_reference'] ?? 'none');
|
||||||
|
|
||||||
|
if ($ref === 'orderpro') {
|
||||||
|
return (string) ($order['internal_order_number'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($ref === 'integration') {
|
||||||
|
return (string) ($order['external_order_id'] ?? '');
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function buildSellerSnapshot(): array
|
||||||
|
{
|
||||||
|
$seller = $this->companySettings->getSettings();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'company_name' => $seller['company_name'] ?? '',
|
||||||
|
'tax_number' => $seller['tax_number'] ?? '',
|
||||||
|
'street' => $seller['street'] ?? '',
|
||||||
|
'city' => $seller['city'] ?? '',
|
||||||
|
'postal_code' => $seller['postal_code'] ?? '',
|
||||||
|
'phone' => $seller['phone'] ?? '',
|
||||||
|
'email' => $seller['email'] ?? '',
|
||||||
|
'bank_account' => $seller['bank_account'] ?? '',
|
||||||
|
'bdo_number' => $seller['bdo_number'] ?? '',
|
||||||
|
'regon' => $seller['regon'] ?? '',
|
||||||
|
'court_register' => $seller['court_register'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $addresses
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private function buildBuyerSnapshot(array $addresses): ?array
|
||||||
|
{
|
||||||
|
$buyerAddress = $this->resolveBuyerAddress($addresses);
|
||||||
|
if ($buyerAddress === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'name' => $buyerAddress['name'] ?? '',
|
||||||
|
'company_name' => $buyerAddress['company_name'] ?? '',
|
||||||
|
'tax_number' => $buyerAddress['company_tax_number'] ?? '',
|
||||||
|
'street' => trim((string) (($buyerAddress['street_name'] ?? '') . ' ' . ($buyerAddress['street_number'] ?? ''))),
|
||||||
|
'city' => $buyerAddress['city'] ?? '',
|
||||||
|
'postal_code' => $buyerAddress['zip_code'] ?? '',
|
||||||
|
'phone' => $buyerAddress['phone'] ?? '',
|
||||||
|
'email' => $buyerAddress['email'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $addresses
|
||||||
|
* @return array<string, mixed>|null
|
||||||
|
*/
|
||||||
|
private function resolveBuyerAddress(array $addresses): ?array
|
||||||
|
{
|
||||||
|
$byType = [];
|
||||||
|
foreach ($addresses as $address) {
|
||||||
|
$type = (string) ($address['address_type'] ?? '');
|
||||||
|
if ($type !== '' && !isset($byType[$type])) {
|
||||||
|
$byType[$type] = $address;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $byType['invoice'] ?? $byType['customer'] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array<string, mixed>> $items
|
||||||
|
* @return array{items:list<array<string,mixed>>,total_gross:float}
|
||||||
|
*/
|
||||||
|
private function buildItemsSnapshot(array $items): array
|
||||||
|
{
|
||||||
|
$itemsSnapshot = [];
|
||||||
|
$totalGross = 0.0;
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$qty = (float) ($item['quantity'] ?? 0);
|
||||||
|
$price = $item['original_price_with_tax'] !== null ? (float) $item['original_price_with_tax'] : 0.0;
|
||||||
|
$lineTotal = $qty * $price;
|
||||||
|
$totalGross += $lineTotal;
|
||||||
|
$itemsSnapshot[] = [
|
||||||
|
'name' => $item['original_name'] ?? '',
|
||||||
|
'quantity' => $qty,
|
||||||
|
'price' => $price,
|
||||||
|
'total' => $lineTotal,
|
||||||
|
'sku' => $item['sku'] ?? '',
|
||||||
|
'ean' => $item['ean'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'items' => $itemsSnapshot,
|
||||||
|
'total_gross' => $totalGross,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
* @return array{chain_id:string,depth:int,executions:list<string>}
|
||||||
|
*/
|
||||||
|
private function getChainState(array $context): array
|
||||||
|
{
|
||||||
|
$raw = $context[self::CHAIN_STATE_KEY] ?? null;
|
||||||
|
$rawState = is_array($raw) ? $raw : [];
|
||||||
|
|
||||||
|
$chainId = trim((string) ($rawState['chain_id'] ?? ''));
|
||||||
|
if ($chainId === '') {
|
||||||
|
$chainId = $this->generateChainId();
|
||||||
|
}
|
||||||
|
|
||||||
|
$depth = max(0, (int) ($rawState['depth'] ?? 0));
|
||||||
|
|
||||||
|
$executionsRaw = $rawState['executions'] ?? [];
|
||||||
|
$executions = [];
|
||||||
|
if (is_array($executionsRaw)) {
|
||||||
|
foreach ($executionsRaw as $executionRaw) {
|
||||||
|
$execution = trim((string) $executionRaw);
|
||||||
|
if ($execution === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$executions[] = $execution;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'chain_id' => $chainId,
|
||||||
|
'depth' => $depth,
|
||||||
|
'executions' => array_values(array_unique($executions)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
* @param array{chain_id:string,depth:int,executions:list<string>} $state
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function withChainState(array $context, array $state): array
|
||||||
|
{
|
||||||
|
$context[self::CHAIN_STATE_KEY] = [
|
||||||
|
'chain_id' => $state['chain_id'],
|
||||||
|
'depth' => $state['depth'],
|
||||||
|
'executions' => $state['executions'],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $context
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function withExecution(array $context, string $executionKey): array
|
||||||
|
{
|
||||||
|
$state = $this->getChainState($context);
|
||||||
|
$state['executions'][] = $executionKey;
|
||||||
|
if (count($state['executions']) > self::MAX_CHAIN_EXECUTIONS) {
|
||||||
|
$state['executions'] = array_slice($state['executions'], -self::MAX_CHAIN_EXECUTIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->withChainState($context, $state);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function buildExecutionKey(string $eventType, int $ruleId): string
|
||||||
|
{
|
||||||
|
if ($ruleId <= 0) {
|
||||||
|
return $eventType . '#unknown';
|
||||||
|
}
|
||||||
|
|
||||||
|
return $eventType . '#' . $ruleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{chain_id:string,depth:int,executions:list<string>} $state
|
||||||
|
*/
|
||||||
|
private function isExecutionAlreadyProcessed(array $state, string $executionKey): bool
|
||||||
|
{
|
||||||
|
return in_array($executionKey, $state['executions'], true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $parentContext
|
||||||
|
* @param array<string, mixed> $eventContext
|
||||||
|
*/
|
||||||
|
private function emitEvent(string $eventType, int $orderId, array $parentContext, array $eventContext): void
|
||||||
|
{
|
||||||
|
$parentState = $this->getChainState($parentContext);
|
||||||
|
if ($parentState['depth'] >= self::MAX_CHAIN_DEPTH) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$childState = $parentState;
|
||||||
|
$childState['depth'] = $parentState['depth'] + 1;
|
||||||
|
|
||||||
|
$childContext = $this->withChainState($eventContext, $childState);
|
||||||
|
$this->trigger($eventType, $orderId, $childContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateChainId(): string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
return bin2hex(random_bytes(8));
|
||||||
|
} catch (Throwable) {
|
||||||
|
return uniqid('chain_', true);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -184,7 +184,9 @@ final class CronHandlerFactory
|
|||||||
$automationRepository,
|
$automationRepository,
|
||||||
$emailService,
|
$emailService,
|
||||||
$ordersRepository,
|
$ordersRepository,
|
||||||
$companySettingsRepository
|
$companySettingsRepository,
|
||||||
|
new ReceiptRepository($this->db),
|
||||||
|
new ReceiptConfigRepository($this->db)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user