This commit is contained in:
2026-03-21 18:50:09 +01:00
parent 3cef4c8247
commit 6547bcff1e
7 changed files with 330 additions and 25 deletions

3
.vscode/ftp-kr.json vendored
View File

@@ -15,6 +15,7 @@
"/.vscode",
"/.serena",
"/.claude",
"CLAUDE.md"
"CLAUDE.md",
"/changelog"
]
}

View File

@@ -97,6 +97,18 @@
"modified": false
},
"files": {},
"ga-db.txt": {
"type": "-",
"size": 2604,
"lmtime": 0,
"modified": false
},
"ga.txt": {
"type": "-",
"size": 4111,
"lmtime": 0,
"modified": false
},
".gitignore": {
"type": "-",
"size": 7,

View File

@@ -70,6 +70,6 @@ When creating or modifying overrides, PrestaShop also needs to rebuild the class
## 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.
- Include touched file paths and relevant line references where possible.

15
changelog/2026-03-21.md Normal file
View 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`

View File

@@ -0,0 +1,5 @@
{
"40102298078155-A": {
"sent_at": "2026-03-21 18:41:03"
}
}

View File

@@ -20,7 +20,12 @@ class OrderProcessor
{
const CODE_WAITING_ACCEPTANCE = 'WAITING_ACCEPTANCE';
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 */
protected $empikClientFactory;
@@ -138,35 +143,191 @@ class OrderProcessor
protected function sendFailureNotification($empikOrderId, Exception $exception, array $orderData = [])
{
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');
$subject = sprintf('[%s] Blad importu zamowienia EMPIK: %s', $shopName, $empikOrderId);
$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(
(int) Configuration::get('PS_LANG_DEFAULT'),
'empik_import_error',
$subject,
[
'{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()),
],
self::ERROR_NOTIFICATION_EMAIL,
null,
Configuration::get('PS_SHOP_EMAIL'),
$shopName,
null,
null,
_PS_MODULE_DIR_ . 'empikmarketplace/mails/'
);
$notificationSent = false;
foreach ($this->getErrorNotificationEmails() as $recipientEmail) {
$sent = Mail::send(
(int) Configuration::get('PS_LANG_DEFAULT'),
'empik_import_error',
$subject,
$templateVars,
$recipientEmail,
null,
Configuration::get('PS_SHOP_EMAIL'),
$shopName,
null,
null,
_PS_MODULE_DIR_ . 'empikmarketplace/mails/'
);
if (!$sent) {
$this->logger->logError(sprintf(
'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) {
$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
* @return string
@@ -178,14 +339,35 @@ class OrderProcessor
}
$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 = [
'order_id' => isset($orderData['order_id']) ? $orderData['order_id'] : 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,
'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,
'has_shipping_address' => !empty($customer['shipping_address']) ? 1 : 0,
'has_billing_address' => !empty($customer['billing_address']) ? 1 : 0,
'customer_email' => $customerEmail,
'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);
@@ -193,6 +375,96 @@ class OrderProcessor
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)
{
$acceptLines = $empikOrder->getAcceptanceLines();