feat(125): invoice_requested import fix + drop legacy is_invoice column
- shopPRO: ShopproOrderMapper jako jedyne zrodlo heurystyki detekcji faktury; mapOrderAggregate() zwraca top-level invoice_detected (transient). - ShopproOrdersSyncService: usunieta wlasna shouldRequestInvoice(); propagacja aggregate['invoice_detected'] do setInvoiceRequested() tylko przy created=true. - Allegro: nowa shouldRequestInvoice(payload) z 4 wzorcami (invoice.required, naturalPerson=false, address.taxId, companyName/address.company.name). Wczesniej tylko invoice.required -> analogiczna luka jak shopPRO. - Migracja 20260513_000113: idempotentny backfill (UPDATE invoice_requested=1 WHERE is_invoice=1 AND invoice_requested=0) + DROP COLUMN orders.is_invoice. Guard przez information_schema.COLUMNS + PREPARE/EXECUTE z ALTER TABLE COMMENT no-op fallbackiem (portable MySQL/MariaDB). - Cleanup is_invoice z OrderImportRepository (INSERT cols/values/params, docstring Phase 112) i OrdersRepository (paginate SELECT, transformOrderRow hydrate). AllegroOrderImportService mapping w mapCheckoutFormPayload tez usuniety (wymuszone konsekwencja DROP COLUMN). - Bugfix #1089: zamowienie shopPRO z firm_nip (bez wants_invoice/invoice.required) ustawia teraz invoice_requested=1 -> UI w zakladce Platnosci zaznacza checkbox, przycisk "Wystaw fakture" widoczny. Pending operator: php bin/migrate.php (XAMPP MySQL online) -> backfill 7 zamowien. Smoke test: re-import shopPRO + nowe Allegro z NIP.
This commit is contained in:
@@ -162,13 +162,13 @@ final class OrderImportRepository
|
||||
'INSERT INTO orders (
|
||||
integration_id, source, source_order_id, external_order_id, external_platform_id, external_platform_account_id,
|
||||
status_code, external_payment_type_id, payment_status, external_carrier_id, external_carrier_account_id,
|
||||
customer_login, is_invoice, is_encrypted, is_canceled_by_buyer, currency,
|
||||
customer_login, is_encrypted, is_canceled_by_buyer, currency,
|
||||
total_without_tax, total_with_tax, total_paid, delivery_price, send_date_min, send_date_max, ordered_at,
|
||||
source_created_at, source_updated_at, preferences_json, payload_json, fetched_at
|
||||
) VALUES (
|
||||
:integration_id, :source, :source_order_id, :external_order_id, :external_platform_id, :external_platform_account_id,
|
||||
:status_code, :external_payment_type_id, :payment_status, :external_carrier_id, :external_carrier_account_id,
|
||||
:customer_login, :is_invoice, :is_encrypted, :is_canceled_by_buyer, :currency,
|
||||
:customer_login, :is_encrypted, :is_canceled_by_buyer, :currency,
|
||||
:total_without_tax, :total_with_tax, :total_paid, :delivery_price, :send_date_min, :send_date_max, :ordered_at,
|
||||
:source_created_at, :source_updated_at, :preferences_json, :payload_json, :fetched_at
|
||||
)'
|
||||
@@ -187,7 +187,7 @@ final class OrderImportRepository
|
||||
* Phase 112-01: Delta-only update for re-import. Touches only fields that legitimately
|
||||
* change at the source between syncs. All other order columns (integration_id, source,
|
||||
* external_*, customer_login, currency, totals other than total_paid, delivery_price,
|
||||
* send_date_*, ordered_at, source_created_at, preferences_json, is_invoice, is_encrypted,
|
||||
* send_date_*, ordered_at, source_created_at, preferences_json, is_encrypted,
|
||||
* external_carrier_*, external_payment_type_id) are NOT overwritten on re-import.
|
||||
*
|
||||
* Phase 119-01: When `payment_status` is unchanged between DB and source payload,
|
||||
@@ -255,7 +255,6 @@ final class OrderImportRepository
|
||||
'external_carrier_id' => $orderData['external_carrier_id'] ?? null,
|
||||
'external_carrier_account_id' => $orderData['external_carrier_account_id'] ?? null,
|
||||
'customer_login' => $orderData['customer_login'] ?? null,
|
||||
'is_invoice' => !empty($orderData['is_invoice']) ? 1 : 0,
|
||||
'is_encrypted' => !empty($orderData['is_encrypted']) ? 1 : 0,
|
||||
'is_canceled_by_buyer' => !empty($orderData['is_canceled_by_buyer']) ? 1 : 0,
|
||||
'currency' => (string) ($orderData['currency'] ?? 'PLN'),
|
||||
|
||||
@@ -169,7 +169,6 @@ final class OrdersRepository
|
||||
o.source_updated_at,
|
||||
o.fetched_at,
|
||||
' . $effectiveOrderedAtSql . ' AS effective_ordered_at,
|
||||
o.is_invoice,
|
||||
o.is_canceled_by_buyer,
|
||||
a.name AS buyer_name,
|
||||
a.email AS buyer_email,
|
||||
@@ -232,7 +231,6 @@ final class OrdersRepository
|
||||
'source_created_at' => (string) ($row['source_created_at'] ?? ''),
|
||||
'source_updated_at' => (string) ($row['source_updated_at'] ?? ''),
|
||||
'fetched_at' => (string) ($row['fetched_at'] ?? ''),
|
||||
'is_invoice' => (int) ($row['is_invoice'] ?? 0) === 1,
|
||||
'is_canceled_by_buyer' => (int) ($row['is_canceled_by_buyer'] ?? 0) === 1,
|
||||
'external_carrier_id' => (string) ($row['external_carrier_id'] ?? ''),
|
||||
'external_payment_type_id' => (string) ($row['external_payment_type_id'] ?? ''),
|
||||
|
||||
@@ -96,11 +96,8 @@ final class AllegroOrderImportService
|
||||
);
|
||||
}
|
||||
|
||||
if ($wasCreated) {
|
||||
$invoiceFlag = is_array($payload['invoice'] ?? null) ? $payload['invoice'] : [];
|
||||
if (!empty($invoiceFlag['required'])) {
|
||||
$this->ordersRepository->setInvoiceRequested($savedOrderId, true);
|
||||
}
|
||||
if ($wasCreated && $this->shouldRequestInvoice($payload)) {
|
||||
$this->ordersRepository->setInvoiceRequested($savedOrderId, true);
|
||||
}
|
||||
|
||||
if ($wasCreated && $this->automationService !== null) {
|
||||
@@ -141,6 +138,37 @@ final class AllegroOrderImportService
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect "klient prosi o fakture" flag from Allegro checkout-form payload.
|
||||
* Triggers on explicit `invoice.required`, business buyer (`naturalPerson=false`),
|
||||
* NIP in invoice address, or explicit company name.
|
||||
*
|
||||
* @param array<string, mixed> $payload
|
||||
*/
|
||||
private function shouldRequestInvoice(array $payload): bool
|
||||
{
|
||||
$invoice = is_array($payload['invoice'] ?? null) ? $payload['invoice'] : null;
|
||||
if ($invoice === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!empty($invoice['required'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (array_key_exists('naturalPerson', $invoice) && $invoice['naturalPerson'] === false) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$address = is_array($invoice['address'] ?? null) ? $invoice['address'] : [];
|
||||
if (!empty($address['taxId'])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$companyName = trim((string) ($invoice['companyName'] ?? $address['company']['name'] ?? ''));
|
||||
return $companyName !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $payload
|
||||
* @return array{
|
||||
@@ -232,7 +260,6 @@ final class AllegroOrderImportService
|
||||
'external_carrier_id' => $deliveryForm !== '' ? $deliveryForm : null,
|
||||
'external_carrier_account_id' => $deliveryMethodId !== '' ? $deliveryMethodId : null,
|
||||
'customer_login' => trim((string) ($buyer['login'] ?? '')),
|
||||
'is_invoice' => !empty($invoice['required']),
|
||||
'is_encrypted' => false,
|
||||
'is_canceled_by_buyer' => in_array($externalStatus, ['cancelled', 'canceled'], true),
|
||||
'currency' => strtoupper($currency),
|
||||
|
||||
@@ -145,7 +145,6 @@ final class ShopproOrderMapper
|
||||
'customer_login' => StringHelper::nullableString((string) $this->readPath($payload, [
|
||||
'buyer_email', 'customer.email', 'buyer.email', 'client.email', 'email', 'customer.login', 'buyer.login',
|
||||
])),
|
||||
'is_invoice' => $this->resolveInvoiceRequested($payload),
|
||||
'is_encrypted' => false,
|
||||
'is_canceled_by_buyer' => false,
|
||||
'currency' => $currency,
|
||||
@@ -185,6 +184,7 @@ final class ShopproOrderMapper
|
||||
'shipments' => $shipments,
|
||||
'notes' => $notes,
|
||||
'status_history' => $statusHistory,
|
||||
'invoice_detected' => $this->resolveInvoiceRequested($payload),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -270,10 +270,8 @@ final class ShopproOrdersSyncService
|
||||
);
|
||||
}
|
||||
|
||||
if ($savedOrderId > 0 && $wasCreated) {
|
||||
if ($this->shouldRequestInvoice($rawOrder)) {
|
||||
$this->orders->setInvoiceRequested($savedOrderId, true);
|
||||
}
|
||||
if ($savedOrderId > 0 && $wasCreated && !empty($aggregate['invoice_detected'])) {
|
||||
$this->orders->setInvoiceRequested($savedOrderId, true);
|
||||
}
|
||||
|
||||
if ($savedOrderId > 0 && $wasCreated && !$wasPaymentTransition && $this->automationService !== null) {
|
||||
@@ -307,36 +305,6 @@ final class ShopproOrdersSyncService
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect "klient prosi o fakture" flag from shopPRO raw payload.
|
||||
* Tries common keys; returns false when none present (manual toggle still possible).
|
||||
*
|
||||
* @param array<string, mixed> $rawOrder
|
||||
*/
|
||||
private function shouldRequestInvoice(array $rawOrder): bool
|
||||
{
|
||||
foreach ([['wants_invoice'], ['invoice_required'], ['invoice', 'required'], ['buyer', 'wants_invoice'], ['buyer', 'invoice']] as $path) {
|
||||
$value = $rawOrder;
|
||||
$found = true;
|
||||
foreach ($path as $key) {
|
||||
if (!is_array($value) || !array_key_exists($key, $value)) {
|
||||
$found = false;
|
||||
break;
|
||||
}
|
||||
$value = $value[$key];
|
||||
}
|
||||
if ($found && (
|
||||
$value === true
|
||||
|| $value === 1
|
||||
|| $value === '1'
|
||||
|| (is_string($value) && in_array(strtolower($value), ['true', 'yes', 'tak'], true))
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $rawIds
|
||||
* @return array<int, true>
|
||||
|
||||
Reference in New Issue
Block a user