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:
2026-05-12 22:11:49 +02:00
parent e7a417bc22
commit 2ab461aaae
15 changed files with 619 additions and 67 deletions

View File

@@ -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'),

View File

@@ -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'] ?? ''),

View File

@@ -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),

View File

@@ -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),
];
}

View File

@@ -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>