update
This commit is contained in:
3
.vscode/ftp-kr.json
vendored
3
.vscode/ftp-kr.json
vendored
@@ -15,6 +15,7 @@
|
|||||||
"/.vscode",
|
"/.vscode",
|
||||||
"/.serena",
|
"/.serena",
|
||||||
"/.claude",
|
"/.claude",
|
||||||
"CLAUDE.md"
|
"CLAUDE.md",
|
||||||
|
"/changelog"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
12
.vscode/ftp-kr.sync.cache.json
vendored
12
.vscode/ftp-kr.sync.cache.json
vendored
@@ -97,6 +97,18 @@
|
|||||||
"modified": false
|
"modified": false
|
||||||
},
|
},
|
||||||
"files": {},
|
"files": {},
|
||||||
|
"ga-db.txt": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 2604,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
|
"ga.txt": {
|
||||||
|
"type": "-",
|
||||||
|
"size": 4111,
|
||||||
|
"lmtime": 0,
|
||||||
|
"modified": false
|
||||||
|
},
|
||||||
".gitignore": {
|
".gitignore": {
|
||||||
"type": "-",
|
"type": "-",
|
||||||
"size": 7,
|
"size": 7,
|
||||||
|
|||||||
@@ -70,6 +70,6 @@ When creating or modifying overrides, PrestaShop also needs to rebuild the class
|
|||||||
|
|
||||||
## Custom Assistant Command
|
## Custom Assistant Command
|
||||||
|
|
||||||
- If the user writes `zapisz-changelog`, create or update monthly changelog file `changelog/YYYY-MM.md` (based on current date).
|
- If the user writes `zapisz-changelog`, create or update monthly changelog file `changelog/YYYY-MM-DD.md` (based on current date).
|
||||||
- Add an entry for the current day with a concise summary of code changes made in the current session.
|
- Add an entry for the current day with a concise summary of code changes made in the current session.
|
||||||
- Include touched file paths and relevant line references where possible.
|
- Include touched file paths and relevant line references where possible.
|
||||||
|
|||||||
15
changelog/2026-03-21.md
Normal file
15
changelog/2026-03-21.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
# 2026-03-21
|
||||||
|
|
||||||
|
## Zmiany
|
||||||
|
|
||||||
|
- Wywołano komendę `zapisz-changelog`.
|
||||||
|
- W repo są aktywne zmiany: rozbudowa `modules/empikmarketplace/src/Processor/OrderProcessor.php` (powiadomienia o błędach importu EMPIK: wielu odbiorców, deduplikacja wysyłki, zapis rejestru powiadomień), dodanie `modules/empikmarketplace/data/import_error_notifications.json`, aktualizacja `CLAUDE.md` (format `zapisz-changelog` na `YYYY-MM-DD`) oraz przejście z `changelog/2026-03.md` na pliki dzienne (`changelog/2026-03-19.md`, `changelog/2026-03-21.md`).
|
||||||
|
|
||||||
|
## Zmienione pliki
|
||||||
|
|
||||||
|
- `CLAUDE.md`
|
||||||
|
- `changelog/2026-03.md` (usunięty)
|
||||||
|
- `changelog/2026-03-19.md` (nowy)
|
||||||
|
- `changelog/2026-03-21.md`
|
||||||
|
- `modules/empikmarketplace/data/import_error_notifications.json` (nowy)
|
||||||
|
- `modules/empikmarketplace/src/Processor/OrderProcessor.php`
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"40102298078155-A": {
|
||||||
|
"sent_at": "2026-03-21 18:41:03"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,7 +20,12 @@ class OrderProcessor
|
|||||||
{
|
{
|
||||||
const CODE_WAITING_ACCEPTANCE = 'WAITING_ACCEPTANCE';
|
const CODE_WAITING_ACCEPTANCE = 'WAITING_ACCEPTANCE';
|
||||||
const CODE_SHIPPING = 'SHIPPING';
|
const CODE_SHIPPING = 'SHIPPING';
|
||||||
const ERROR_NOTIFICATION_EMAIL = 'jacek.pyziak@project-pro.pl';
|
const ERROR_NOTIFICATION_EMAILS = [
|
||||||
|
'jacek.pyziak@project-pro.pl',
|
||||||
|
'biuro@interblue.pl',
|
||||||
|
'wxkwasnik@gmail.com',
|
||||||
|
];
|
||||||
|
const ERROR_NOTIFICATION_STORE_FILE = 'import_error_notifications.json';
|
||||||
|
|
||||||
/** @var EmpikClientFactory */
|
/** @var EmpikClientFactory */
|
||||||
protected $empikClientFactory;
|
protected $empikClientFactory;
|
||||||
@@ -138,35 +143,191 @@ class OrderProcessor
|
|||||||
protected function sendFailureNotification($empikOrderId, Exception $exception, array $orderData = [])
|
protected function sendFailureNotification($empikOrderId, Exception $exception, array $orderData = [])
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
|
$empikNotificationOrderId = $this->resolveEmpikNotificationOrderId($empikOrderId, $orderData);
|
||||||
|
|
||||||
|
if ($this->wasFailureNotificationSent($empikNotificationOrderId)) {
|
||||||
|
$this->logger->logError(sprintf(
|
||||||
|
'Skipping duplicate EMPIK import error notification for order [%s] - notification already sent.',
|
||||||
|
$empikNotificationOrderId
|
||||||
|
));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$shopName = Configuration::get('PS_SHOP_NAME');
|
$shopName = Configuration::get('PS_SHOP_NAME');
|
||||||
$subject = sprintf('[%s] Blad importu zamowienia EMPIK: %s', $shopName, $empikOrderId);
|
$subject = sprintf('[%s] Blad importu zamowienia EMPIK: %s', $shopName, $empikOrderId);
|
||||||
$orderContext = $this->buildOrderContext($orderData);
|
$orderContext = $this->buildOrderContext($orderData);
|
||||||
|
$templateVars = [
|
||||||
|
'{empik_order_id}' => $empikOrderId,
|
||||||
|
'{error_message}' => $exception->getMessage(),
|
||||||
|
'{error_date}' => date('Y-m-d H:i:s'),
|
||||||
|
'{order_context}' => $orderContext,
|
||||||
|
'{order_context_html}' => nl2br(htmlspecialchars($orderContext, ENT_QUOTES, 'UTF-8')),
|
||||||
|
'{stack_trace}' => nl2br($exception->getTraceAsString()),
|
||||||
|
];
|
||||||
|
|
||||||
Mail::send(
|
$notificationSent = false;
|
||||||
(int) Configuration::get('PS_LANG_DEFAULT'),
|
foreach ($this->getErrorNotificationEmails() as $recipientEmail) {
|
||||||
'empik_import_error',
|
$sent = Mail::send(
|
||||||
$subject,
|
(int) Configuration::get('PS_LANG_DEFAULT'),
|
||||||
[
|
'empik_import_error',
|
||||||
'{empik_order_id}' => $empikOrderId,
|
$subject,
|
||||||
'{error_message}' => $exception->getMessage(),
|
$templateVars,
|
||||||
'{error_date}' => date('Y-m-d H:i:s'),
|
$recipientEmail,
|
||||||
'{order_context}' => $orderContext,
|
null,
|
||||||
'{order_context_html}' => nl2br(htmlspecialchars($orderContext, ENT_QUOTES, 'UTF-8')),
|
Configuration::get('PS_SHOP_EMAIL'),
|
||||||
'{stack_trace}' => nl2br($exception->getTraceAsString()),
|
$shopName,
|
||||||
],
|
null,
|
||||||
self::ERROR_NOTIFICATION_EMAIL,
|
null,
|
||||||
null,
|
_PS_MODULE_DIR_ . 'empikmarketplace/mails/'
|
||||||
Configuration::get('PS_SHOP_EMAIL'),
|
);
|
||||||
$shopName,
|
|
||||||
null,
|
if (!$sent) {
|
||||||
null,
|
$this->logger->logError(sprintf(
|
||||||
_PS_MODULE_DIR_ . 'empikmarketplace/mails/'
|
'Failed to send EMPIK import error notification for order [%s] to [%s].',
|
||||||
);
|
$empikOrderId,
|
||||||
|
$recipientEmail
|
||||||
|
));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$notificationSent = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($notificationSent) {
|
||||||
|
$this->markFailureNotificationAsSent($empikNotificationOrderId);
|
||||||
|
} else {
|
||||||
|
$this->logger->logError(sprintf(
|
||||||
|
'EMPIK import error notification was not sent to any recipient for order [%s].',
|
||||||
|
$empikOrderId
|
||||||
|
));
|
||||||
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
$this->logger->logError(sprintf('Error sending failure notification email: %s', $e->getMessage()));
|
$this->logger->logError(sprintf('Error sending failure notification email: %s', $e->getMessage()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function getErrorNotificationEmails()
|
||||||
|
{
|
||||||
|
return self::ERROR_NOTIFICATION_EMAILS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deduplication key must always be EMPIK order_id from API payload.
|
||||||
|
*
|
||||||
|
* @param string $fallbackOrderId
|
||||||
|
* @param array $orderData
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function resolveEmpikNotificationOrderId($fallbackOrderId, array $orderData)
|
||||||
|
{
|
||||||
|
if (!empty($orderData['order_id']) && is_string($orderData['order_id'])) {
|
||||||
|
return $orderData['order_id'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($fallbackOrderId) && $fallbackOrderId !== 'unknown') {
|
||||||
|
return $fallbackOrderId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $empikOrderId
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
protected function wasFailureNotificationSent($empikOrderId)
|
||||||
|
{
|
||||||
|
if (!$empikOrderId) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$notifications = $this->loadNotificationRegistry();
|
||||||
|
|
||||||
|
return isset($notifications[$empikOrderId]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $empikOrderId
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function markFailureNotificationAsSent($empikOrderId)
|
||||||
|
{
|
||||||
|
if (!$empikOrderId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$notifications = $this->loadNotificationRegistry();
|
||||||
|
$notifications[$empikOrderId] = [
|
||||||
|
'sent_at' => date('Y-m-d H:i:s'),
|
||||||
|
];
|
||||||
|
|
||||||
|
$this->saveNotificationRegistry($notifications);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getNotificationRegistryPath()
|
||||||
|
{
|
||||||
|
return _PS_MODULE_DIR_ . 'empikmarketplace/data/' . self::ERROR_NOTIFICATION_STORE_FILE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function loadNotificationRegistry()
|
||||||
|
{
|
||||||
|
$path = $this->getNotificationRegistryPath();
|
||||||
|
|
||||||
|
if (!file_exists($path)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw = @file_get_contents($path);
|
||||||
|
if ($raw === false || $raw === '') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$decoded = json_decode($raw, true);
|
||||||
|
if (!is_array($decoded)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $decoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $notifications
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
protected function saveNotificationRegistry(array $notifications)
|
||||||
|
{
|
||||||
|
$path = $this->getNotificationRegistryPath();
|
||||||
|
$directory = dirname($path);
|
||||||
|
|
||||||
|
if (!is_dir($directory)) {
|
||||||
|
@mkdir($directory, 0755, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = json_encode($notifications, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
|
if ($json === false) {
|
||||||
|
$this->logger->logError('Unable to serialize EMPIK import error notification registry.');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (@file_put_contents($path, $json, LOCK_EX) === false) {
|
||||||
|
$this->logger->logError(sprintf(
|
||||||
|
'Unable to save EMPIK import error notification registry to [%s].',
|
||||||
|
$path
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array $orderData
|
* @param array $orderData
|
||||||
* @return string
|
* @return string
|
||||||
@@ -178,14 +339,35 @@ class OrderProcessor
|
|||||||
}
|
}
|
||||||
|
|
||||||
$customer = isset($orderData['customer']) && is_array($orderData['customer']) ? $orderData['customer'] : [];
|
$customer = isset($orderData['customer']) && is_array($orderData['customer']) ? $orderData['customer'] : [];
|
||||||
|
$shippingAddress = isset($customer['shipping_address']) && is_array($customer['shipping_address']) ? $customer['shipping_address'] : [];
|
||||||
|
$billingAddress = isset($customer['billing_address']) && is_array($customer['billing_address']) ? $customer['billing_address'] : [];
|
||||||
|
$additionalFields = isset($orderData['order_additional_fields']) && is_array($orderData['order_additional_fields']) ? $orderData['order_additional_fields'] : [];
|
||||||
|
$customerEmail = $this->extractAdditionalFieldValue($additionalFields, 'customer-email');
|
||||||
|
|
||||||
$summary = [
|
$summary = [
|
||||||
'order_id' => isset($orderData['order_id']) ? $orderData['order_id'] : null,
|
'order_id' => isset($orderData['order_id']) ? $orderData['order_id'] : null,
|
||||||
'order_state' => isset($orderData['order_state']) ? $orderData['order_state'] : null,
|
'order_state' => isset($orderData['order_state']) ? $orderData['order_state'] : null,
|
||||||
|
'created_date' => isset($orderData['created_date']) ? $orderData['created_date'] : null,
|
||||||
|
'updated_date' => isset($orderData['updated_date']) ? $orderData['updated_date'] : null,
|
||||||
|
'currency_iso_code' => isset($orderData['currency_iso_code']) ? $orderData['currency_iso_code'] : null,
|
||||||
|
'payment_type' => isset($orderData['payment_type']) ? $orderData['payment_type'] : null,
|
||||||
|
'shipping_type_code' => isset($orderData['shipping_type_code']) ? $orderData['shipping_type_code'] : null,
|
||||||
|
'shipping_type_label' => isset($orderData['shipping_type_label']) ? $orderData['shipping_type_label'] : null,
|
||||||
'total_price' => isset($orderData['total_price']) ? $orderData['total_price'] : null,
|
'total_price' => isset($orderData['total_price']) ? $orderData['total_price'] : null,
|
||||||
'shipping_price' => isset($orderData['shipping_price']) ? $orderData['shipping_price'] : null,
|
'shipping_price' => isset($orderData['shipping_price']) ? $orderData['shipping_price'] : null,
|
||||||
'order_lines_count' => isset($orderData['order_lines']) && is_array($orderData['order_lines']) ? count($orderData['order_lines']) : 0,
|
'order_lines_count' => isset($orderData['order_lines']) && is_array($orderData['order_lines']) ? count($orderData['order_lines']) : 0,
|
||||||
'has_shipping_address' => !empty($customer['shipping_address']) ? 1 : 0,
|
'customer_email' => $customerEmail,
|
||||||
'has_billing_address' => !empty($customer['billing_address']) ? 1 : 0,
|
'has_shipping_address' => !empty($shippingAddress) ? 1 : 0,
|
||||||
|
'has_billing_address' => !empty($billingAddress) ? 1 : 0,
|
||||||
|
'customer' => [
|
||||||
|
'firstname' => isset($customer['firstname']) ? $customer['firstname'] : null,
|
||||||
|
'lastname' => isset($customer['lastname']) ? $customer['lastname'] : null,
|
||||||
|
'email' => $customerEmail,
|
||||||
|
],
|
||||||
|
'shipping_address' => $this->buildAddressContext($shippingAddress),
|
||||||
|
'billing_address' => $this->buildAddressContext($billingAddress),
|
||||||
|
'order_lines' => $this->buildOrderLinesContext(isset($orderData['order_lines']) && is_array($orderData['order_lines']) ? $orderData['order_lines'] : []),
|
||||||
|
'order_additional_fields' => $this->buildAdditionalFieldsContext($additionalFields),
|
||||||
];
|
];
|
||||||
|
|
||||||
$json = json_encode($summary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
$json = json_encode($summary, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||||
@@ -193,6 +375,96 @@ class OrderProcessor
|
|||||||
return $json !== false ? $json : 'Unable to serialize order payload summary.';
|
return $json !== false ? $json : 'Unable to serialize order payload summary.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $addressData
|
||||||
|
* @return array|null
|
||||||
|
*/
|
||||||
|
protected function buildAddressContext(array $addressData)
|
||||||
|
{
|
||||||
|
if (empty($addressData)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'firstname' => isset($addressData['firstname']) ? $addressData['firstname'] : null,
|
||||||
|
'lastname' => isset($addressData['lastname']) ? $addressData['lastname'] : null,
|
||||||
|
'company' => isset($addressData['company']) ? $addressData['company'] : null,
|
||||||
|
'street_1' => isset($addressData['street_1']) ? $addressData['street_1'] : null,
|
||||||
|
'street_2' => isset($addressData['street_2']) ? $addressData['street_2'] : null,
|
||||||
|
'city' => isset($addressData['city']) ? $addressData['city'] : null,
|
||||||
|
'zip_code' => isset($addressData['zip_code']) ? $addressData['zip_code'] : null,
|
||||||
|
'country_iso_code' => isset($addressData['country_iso_code']) ? $addressData['country_iso_code'] : null,
|
||||||
|
'phone' => isset($addressData['phone']) ? $addressData['phone'] : null,
|
||||||
|
'additional_info' => isset($addressData['additional_info']) ? $addressData['additional_info'] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $orderLines
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function buildOrderLinesContext(array $orderLines)
|
||||||
|
{
|
||||||
|
$context = [];
|
||||||
|
|
||||||
|
foreach ($orderLines as $line) {
|
||||||
|
$context[] = [
|
||||||
|
'order_line_id' => isset($line['order_line_id']) ? $line['order_line_id'] : null,
|
||||||
|
'offer_sku' => isset($line['offer_sku']) ? $line['offer_sku'] : null,
|
||||||
|
'offer_id' => isset($line['offer_id']) ? $line['offer_id'] : null,
|
||||||
|
'product_title' => isset($line['product_title']) ? $line['product_title'] : null,
|
||||||
|
'quantity' => isset($line['quantity']) ? $line['quantity'] : null,
|
||||||
|
'price_unit' => isset($line['price_unit']) ? $line['price_unit'] : null,
|
||||||
|
'price' => isset($line['price']) ? $line['price'] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $additionalFields
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function buildAdditionalFieldsContext(array $additionalFields)
|
||||||
|
{
|
||||||
|
$context = [];
|
||||||
|
|
||||||
|
foreach ($additionalFields as $field) {
|
||||||
|
if (!is_array($field)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$context[] = [
|
||||||
|
'code' => isset($field['code']) ? $field['code'] : null,
|
||||||
|
'type' => isset($field['type']) ? $field['type'] : null,
|
||||||
|
'value' => isset($field['value']) ? $field['value'] : null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $context;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array $additionalFields
|
||||||
|
* @param string $code
|
||||||
|
* @return string|null
|
||||||
|
*/
|
||||||
|
protected function extractAdditionalFieldValue(array $additionalFields, $code)
|
||||||
|
{
|
||||||
|
foreach ($additionalFields as $field) {
|
||||||
|
if (!is_array($field)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($field['code']) && $field['code'] === $code) {
|
||||||
|
return isset($field['value']) ? $field['value'] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
protected function accept(EmpikOrderWrapper $empikOrder)
|
protected function accept(EmpikOrderWrapper $empikOrder)
|
||||||
{
|
{
|
||||||
$acceptLines = $empikOrder->getAcceptanceLines();
|
$acceptLines = $empikOrder->getAcceptanceLines();
|
||||||
|
|||||||
Reference in New Issue
Block a user