refactor(01-tech-debt): extract AllegroTokenManager and StringHelper

Phase 1 complete (2/2 plans):

- Plan 01-01: Extract AllegroTokenManager — OAuth token logic
  centralized from 4 classes into dedicated manager class

- Plan 01-02: Extract StringHelper — nullableString/normalizeDateTime/
  normalizeColorHex extracted from 15+ classes into App\Core\Support\StringHelper;
  removed 19 duplicate private methods

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 23:36:06 +01:00
parent 4c3daf69b7
commit f8db8c0162
26 changed files with 1374 additions and 547 deletions

View File

@@ -3,6 +3,7 @@ declare(strict_types=1);
namespace App\Modules\Settings;
use App\Core\Support\StringHelper;
use App\Modules\Orders\OrderImportRepository;
use App\Modules\Orders\OrdersRepository;
use DateTimeImmutable;
@@ -63,8 +64,8 @@ final class ShopproOrdersSyncService
try {
$statusMap = $this->buildStatusMap($integrationId);
$cursorUpdatedAt = $this->nullableString((string) ($state['last_synced_updated_at'] ?? ''));
$cursorOrderId = $this->nullableString((string) ($state['last_synced_source_order_id'] ?? ''));
$cursorUpdatedAt = StringHelper::nullableString((string) ($state['last_synced_updated_at'] ?? ''));
$cursorOrderId = StringHelper::nullableString((string) ($state['last_synced_source_order_id'] ?? ''));
$startDate = $this->resolveStartDate(
(string) ($integration['orders_fetch_start_date'] ?? ''),
$cursorUpdatedAt
@@ -265,7 +266,7 @@ final class ShopproOrdersSyncService
private function resolveStartDate(string $settingsDate, ?string $cursorUpdatedAt): ?string
{
$settings = trim($settingsDate);
$cursor = $this->nullableString((string) $cursorUpdatedAt);
$cursor = StringHelper::nullableString((string) $cursorUpdatedAt);
if ($settings === '' && $cursor === null) {
return null;
}
@@ -288,7 +289,7 @@ final class ShopproOrdersSyncService
$result = [];
foreach ($items as $row) {
$sourceOrderId = $this->normalizeOrderId($this->readPath($row, ['id', 'order_id', 'external_order_id']));
$sourceUpdatedAt = $this->normalizeDateTime((string) $this->readPath($row, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at', 'date_created']));
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($row, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at', 'date_created']));
if ($sourceOrderId === '' || $sourceUpdatedAt === null) {
continue;
}
@@ -360,8 +361,8 @@ final class ShopproOrdersSyncService
$sourceOrderId = $fallbackOrderId;
}
$sourceCreatedAt = $this->normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add']));
$sourceUpdatedAt = $this->normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at']));
$sourceCreatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['created_at', 'date_created', 'date_add']));
$sourceUpdatedAt = StringHelper::normalizeDateTime((string) $this->readPath($payload, ['updated_at', 'date_updated', 'modified_at', 'date_modified', 'created_at']));
if ($sourceUpdatedAt === null) {
$sourceUpdatedAt = $fallbackUpdatedAt !== '' ? $fallbackUpdatedAt : date('Y-m-d H:i:s');
}
@@ -425,13 +426,13 @@ final class ShopproOrdersSyncService
'external_platform_id' => 'shoppro',
'external_platform_account_id' => null,
'external_status_id' => $effectiveStatus,
'external_payment_type_id' => $this->nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
'external_payment_type_id' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method', 'payments.method'])),
'payment_status' => $this->mapPaymentStatus($payload, $isPaid),
'external_carrier_id' => $this->nullableString($deliveryLabel),
'external_carrier_account_id' => $this->nullableString((string) $this->readPath($payload, [
'external_carrier_id' => StringHelper::nullableString($deliveryLabel),
'external_carrier_account_id' => StringHelper::nullableString((string) $this->readPath($payload, [
'transport_id', 'shipping.method_id', 'delivery.method_id',
])),
'customer_login' => $this->nullableString((string) $this->readPath($payload, [
'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),
@@ -484,17 +485,17 @@ final class ShopproOrdersSyncService
{
$result = [];
$customerFirstName = $this->nullableString((string) $this->readPath($payload, [
$customerFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer.first_name', 'buyer.firstname', 'customer.first_name', 'customer.firstname',
'client.first_name', 'client.firstname', 'billing_address.first_name', 'billing_address.firstname',
'first_name', 'firstname', 'client_name', 'imie',
]));
$customerLastName = $this->nullableString((string) $this->readPath($payload, [
$customerLastName = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer.last_name', 'buyer.lastname', 'customer.last_name', 'customer.lastname',
'client.last_name', 'client.lastname', 'billing_address.last_name', 'billing_address.lastname',
'last_name', 'lastname', 'client_surname', 'nazwisko',
]));
$customerName = $this->nullableString((string) $this->readPath($payload, [
$customerName = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_name', 'buyer.name', 'customer.name', 'client.name', 'billing_address.name',
'receiver.name', 'client', 'customer_full_name', 'client_full_name',
]));
@@ -502,11 +503,11 @@ final class ShopproOrdersSyncService
$customerName = $this->composeName($customerFirstName, $customerLastName, 'Klient');
}
$customerEmail = $this->nullableString((string) $this->readPath($payload, [
$customerEmail = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_email', 'buyer.email', 'customer.email', 'client.email', 'billing_address.email',
'shipping_address.email', 'delivery_address.email', 'email', 'client_email', 'mail',
]));
$customerPhone = $this->nullableString((string) $this->readPath($payload, [
$customerPhone = StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_phone', 'buyer.phone', 'customer.phone', 'client.phone', 'billing_address.phone',
'shipping_address.phone', 'delivery_address.phone', 'phone', 'telephone', 'client_phone',
'phone_number', 'client_phone_number',
@@ -517,25 +518,25 @@ final class ShopproOrdersSyncService
'name' => $customerName ?? 'Klient',
'phone' => $customerPhone,
'email' => $customerEmail,
'street_name' => $this->nullableString((string) $this->readPath($payload, [
'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.street', 'customer.address.street', 'billing_address.street', 'client.address.street',
'address.street', 'street', 'client_street', 'ulica',
])),
'street_number' => $this->nullableString((string) $this->readPath($payload, [
'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.street_number', 'customer.address.street_number', 'billing_address.street_number',
'billing_address.house_number', 'client.address.street_number', 'address.street_number',
'house_number', 'street_no', 'street_number', 'nr_domu',
])),
'city' => $this->nullableString((string) $this->readPath($payload, [
'city' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.city', 'customer.address.city', 'billing_address.city', 'client.address.city',
'address.city', 'city', 'client_city', 'miejscowosc',
])),
'zip_code' => $this->nullableString((string) $this->readPath($payload, [
'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.zip', 'buyer_address.postcode', 'customer.address.zip', 'customer.address.postcode',
'billing_address.zip', 'billing_address.postcode', 'client.address.zip', 'address.zip',
'address.postcode', 'zip', 'postcode', 'client_postal_code', 'kod_pocztowy',
])),
'country' => $this->nullableString((string) $this->readPath($payload, [
'country' => StringHelper::nullableString((string) $this->readPath($payload, [
'buyer_address.country', 'customer.address.country', 'billing_address.country', 'client.address.country',
'address.country', 'country', 'kraj',
])),
@@ -553,17 +554,17 @@ final class ShopproOrdersSyncService
$result[] = $invoiceAddress;
}
$deliveryFirstName = $this->nullableString((string) $this->readPath($payload, [
$deliveryFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.first_name', 'delivery.address.firstname', 'shipping.address.first_name', 'shipping.address.firstname',
'delivery_address.first_name', 'delivery_address.firstname', 'shipping_address.first_name', 'shipping_address.firstname',
'receiver.first_name', 'receiver.firstname', 'delivery_first_name', 'shipping_first_name',
]));
$deliveryLastName = $this->nullableString((string) $this->readPath($payload, [
$deliveryLastName = StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.last_name', 'delivery.address.lastname', 'shipping.address.last_name', 'shipping.address.lastname',
'delivery_address.last_name', 'delivery_address.lastname', 'shipping_address.last_name', 'shipping_address.lastname',
'receiver.last_name', 'receiver.lastname', 'delivery_last_name', 'shipping_last_name',
]));
$deliveryName = $this->nullableString((string) $this->readPath($payload, [
$deliveryName = StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.name', 'shipping.address.name', 'delivery_address.name', 'shipping_address.name',
'receiver.name', 'delivery_name', 'shipping_name',
]));
@@ -574,39 +575,39 @@ final class ShopproOrdersSyncService
$pickupData = $this->parsePickupPoint((string) $this->readPath($payload, ['inpost_paczkomat', 'orlen_point', 'pickup_point']));
$deliveryAddress = [
'name' => $deliveryName,
'phone' => $this->nullableString((string) $this->readPath($payload, [
'phone' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.phone', 'shipping.address.phone', 'delivery_address.phone', 'shipping_address.phone',
'receiver.phone', 'delivery_phone', 'shipping_phone',
])) ?? $customerPhone,
'email' => $this->nullableString((string) $this->readPath($payload, [
'email' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.email', 'shipping.address.email', 'delivery_address.email', 'shipping_address.email',
'receiver.email', 'delivery_email', 'shipping_email',
])) ?? $customerEmail,
'street_name' => $this->nullableString((string) $this->readPath($payload, [
'street_name' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.street', 'shipping.address.street', 'delivery_address.street', 'shipping_address.street',
'receiver.address.street', 'delivery_street', 'shipping_street',
])) ?? $this->nullableString($pickupData['street'] ?? ''),
'street_number' => $this->nullableString((string) $this->readPath($payload, [
])) ?? StringHelper::nullableString($pickupData['street'] ?? ''),
'street_number' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.street_number', 'shipping.address.street_number', 'delivery_address.street_number', 'shipping_address.street_number',
'delivery.address.house_number', 'shipping.address.house_number', 'receiver.address.street_number',
'receiver.address.house_number', 'delivery_street_number', 'shipping_street_number',
])),
'city' => $this->nullableString((string) $this->readPath($payload, [
'city' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.city', 'shipping.address.city', 'delivery_address.city', 'shipping_address.city',
'receiver.address.city', 'delivery_city', 'shipping_city',
])) ?? $this->nullableString($pickupData['city'] ?? ''),
'zip_code' => $this->nullableString((string) $this->readPath($payload, [
])) ?? StringHelper::nullableString($pickupData['city'] ?? ''),
'zip_code' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.zip', 'delivery.address.postcode', 'shipping.address.zip', 'shipping.address.postcode',
'delivery_address.zip', 'delivery_address.postcode', 'shipping_address.zip', 'shipping_address.postcode',
'receiver.address.zip', 'receiver.address.postcode', 'delivery_zip', 'delivery_postcode',
'shipping_zip', 'shipping_postcode',
])) ?? $this->nullableString($pickupData['zip_code'] ?? ''),
'country' => $this->nullableString((string) $this->readPath($payload, [
])) ?? StringHelper::nullableString($pickupData['zip_code'] ?? ''),
'country' => StringHelper::nullableString((string) $this->readPath($payload, [
'delivery.address.country', 'shipping.address.country', 'delivery_address.country', 'shipping_address.country',
'receiver.address.country', 'delivery_country', 'shipping_country',
])),
'parcel_external_id' => $this->nullableString($pickupData['code'] ?? ''),
'parcel_name' => $this->nullableString($pickupData['label'] ?? ''),
'parcel_external_id' => StringHelper::nullableString($pickupData['code'] ?? ''),
'parcel_name' => StringHelper::nullableString($pickupData['label'] ?? ''),
'payload_json' => [
'delivery' => $this->readPath($payload, ['delivery']),
'shipping' => $this->readPath($payload, ['shipping']),
@@ -619,7 +620,7 @@ final class ShopproOrdersSyncService
];
if (($deliveryAddress['name'] ?? null) === null) {
$deliveryAddress['name'] = $this->nullableString($this->buildDeliveryMethodLabel($payload));
$deliveryAddress['name'] = StringHelper::nullableString($this->buildDeliveryMethodLabel($payload));
}
$hasDeliveryData = $this->hasAddressData($deliveryAddress);
@@ -653,11 +654,11 @@ final class ShopproOrdersSyncService
return true;
}
$companyName = $this->nullableString((string) $this->readPath($payload, [
$companyName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
'firm_name', 'company_name', 'client_company', 'buyer_company',
]));
$taxNumber = $this->nullableString((string) $this->readPath($payload, [
$taxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
'firm_nip', 'company_nip', 'tax_id', 'nip',
]));
@@ -675,50 +676,50 @@ final class ShopproOrdersSyncService
?string $customerEmail,
?string $customerPhone
): ?array {
$companyName = $this->nullableString((string) $this->readPath($payload, [
$companyName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.company_name', 'invoice.company', 'billing.company_name', 'billing.company',
'firm_name', 'company_name', 'client_company', 'buyer_company',
]));
$companyTaxNumber = $this->nullableString((string) $this->readPath($payload, [
$companyTaxNumber = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.tax_id', 'invoice.nip', 'billing.tax_id', 'billing.nip',
'firm_nip', 'company_nip', 'tax_id', 'nip',
]));
$invoiceFirstName = $this->nullableString((string) $this->readPath($payload, [
$invoiceFirstName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.first_name', 'invoice.firstname', 'billing_address.first_name', 'billing_address.firstname',
'buyer.first_name', 'customer.first_name', 'client_name',
]));
$invoiceLastName = $this->nullableString((string) $this->readPath($payload, [
$invoiceLastName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.last_name', 'invoice.lastname', 'billing_address.last_name', 'billing_address.lastname',
'buyer.last_name', 'customer.last_name', 'client_surname',
]));
$invoiceName = $companyName ?? $this->composeName($invoiceFirstName, $invoiceLastName, $customerName ?? 'Faktura');
$streetName = $this->nullableString((string) $this->readPath($payload, [
$streetName = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.street', 'invoice.street', 'billing_address.street', 'billing.street',
'firm_street', 'company_street',
]));
$streetNumber = $this->nullableString((string) $this->readPath($payload, [
$streetNumber = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.street_number', 'invoice.street_number', 'invoice.house_number',
'billing_address.street_number', 'billing_address.house_number',
'billing.street_number', 'house_number', 'street_number',
]));
$city = $this->nullableString((string) $this->readPath($payload, [
$city = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.city', 'invoice.city', 'billing_address.city', 'billing.city',
'firm_city', 'company_city',
]));
$zipCode = $this->nullableString((string) $this->readPath($payload, [
$zipCode = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.zip', 'invoice.address.postcode', 'invoice.zip', 'invoice.postcode',
'billing_address.zip', 'billing_address.postcode', 'billing.zip', 'billing.postcode',
'firm_postal_code', 'company_postal_code',
]));
$country = $this->nullableString((string) $this->readPath($payload, [
$country = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.address.country', 'invoice.country', 'billing_address.country', 'billing.country',
'firm_country', 'company_country',
]));
$email = $this->nullableString((string) $this->readPath($payload, [
$email = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.email', 'billing_address.email', 'billing.email', 'client_email',
])) ?? $customerEmail;
$phone = $this->nullableString((string) $this->readPath($payload, [
$phone = StringHelper::nullableString((string) $this->readPath($payload, [
'invoice.phone', 'billing_address.phone', 'billing.phone', 'client_phone',
])) ?? $customerPhone;
@@ -814,31 +815,31 @@ final class ShopproOrdersSyncService
$productId = (int) $this->readPath($row, ['product_id']);
$parentProductId = (int) $this->readPath($row, ['parent_product_id']);
$mediaUrl = $this->nullableString((string) $this->readPath($row, ['image', 'image_url', 'img_url', 'img', 'photo', 'photo_url']));
$mediaUrl = StringHelper::nullableString((string) $this->readPath($row, ['image', 'image_url', 'img_url', 'img', 'photo', 'photo_url']));
if ($mediaUrl === null && $productId > 0 && isset($productImagesById[$productId])) {
$mediaUrl = $this->nullableString((string) $productImagesById[$productId]);
$mediaUrl = StringHelper::nullableString((string) $productImagesById[$productId]);
}
if ($mediaUrl === null && $parentProductId > 0 && isset($productImagesById[$parentProductId])) {
$mediaUrl = $this->nullableString((string) $productImagesById[$parentProductId]);
$mediaUrl = StringHelper::nullableString((string) $productImagesById[$parentProductId]);
}
$result[] = [
'source_item_id' => $this->nullableString((string) $this->readPath($row, ['id', 'item_id'])),
'external_item_id' => $this->nullableString((string) $this->readPath($row, ['id', 'item_id'])),
'ean' => $this->nullableString((string) $this->readPath($row, ['ean'])),
'sku' => $this->nullableString((string) $this->readPath($row, ['sku', 'symbol', 'code'])),
'source_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
'external_item_id' => StringHelper::nullableString((string) $this->readPath($row, ['id', 'item_id'])),
'ean' => StringHelper::nullableString((string) $this->readPath($row, ['ean'])),
'sku' => StringHelper::nullableString((string) $this->readPath($row, ['sku', 'symbol', 'code'])),
'original_name' => $name,
'original_code' => $this->nullableString((string) $this->readPath($row, ['code', 'symbol'])),
'original_code' => StringHelper::nullableString((string) $this->readPath($row, ['code', 'symbol'])),
'original_price_with_tax' => $this->toFloatOrNull($this->readPath($row, ['price_gross', 'gross_price', 'price', 'price_brutto'])),
'original_price_without_tax' => $this->toFloatOrNull($this->readPath($row, ['price_net', 'net_price', 'price_netto'])),
'media_url' => $mediaUrl,
'quantity' => $this->toFloatOrDefault($this->readPath($row, ['quantity', 'qty']), 1.0),
'tax_rate' => $this->toFloatOrNull($this->readPath($row, ['vat', 'tax_rate'])),
'item_status' => null,
'unit' => $this->nullableString((string) $this->readPath($row, ['unit'])),
'unit' => StringHelper::nullableString((string) $this->readPath($row, ['unit'])),
'item_type' => 'product',
'source_product_id' => $this->nullableString((string) ($productId > 0 ? $productId : $parentProductId)),
'source_product_set_id' => $this->nullableString((string) ($parentProductId > 0 ? $parentProductId : '')),
'source_product_id' => StringHelper::nullableString((string) ($productId > 0 ? $productId : $parentProductId)),
'source_product_set_id' => StringHelper::nullableString((string) ($parentProductId > 0 ? $parentProductId : '')),
'sort_order' => $sort++,
'payload_json' => $row,
];
@@ -853,7 +854,7 @@ final class ShopproOrdersSyncService
*/
private function mapPayments(array $payload, string $currency, ?float $totalPaid): array
{
$paymentMethod = $this->nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method']));
$paymentMethod = StringHelper::nullableString((string) $this->readPath($payload, ['payment_method', 'payment.method']));
if ($paymentMethod === null && $totalPaid === null) {
return [];
}
@@ -862,10 +863,10 @@ final class ShopproOrdersSyncService
'source_payment_id' => null,
'external_payment_id' => null,
'payment_type_id' => $paymentMethod ?? 'unknown',
'payment_date' => $this->nullableString((string) $this->readPath($payload, ['payment_date', 'payment.date'])),
'payment_date' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_date', 'payment.date'])),
'amount' => $totalPaid,
'currency' => $currency,
'comment' => $this->nullableString((string) $this->readPath($payload, ['payment_status', 'payment.status'])),
'comment' => StringHelper::nullableString((string) $this->readPath($payload, ['payment_status', 'payment.status'])),
'payload_json' => null,
]];
}
@@ -876,7 +877,7 @@ final class ShopproOrdersSyncService
*/
private function mapShipments(array $payload): array
{
$tracking = $this->nullableString((string) $this->readPath($payload, ['delivery_tracking_number', 'delivery.tracking_number', 'shipping.tracking_number']));
$tracking = StringHelper::nullableString((string) $this->readPath($payload, ['delivery_tracking_number', 'delivery.tracking_number', 'shipping.tracking_number']));
if ($tracking === null) {
return [];
}
@@ -888,7 +889,7 @@ final class ShopproOrdersSyncService
'carrier_provider_id' => $this->sanitizePlainText((string) ($this->readPath($payload, [
'delivery_method', 'shipping.method', 'transport', 'transport_description',
]) ?? 'unknown')),
'posted_at' => $this->nullableString((string) $this->readPath($payload, ['delivery.posted_at', 'shipping.posted_at'])),
'posted_at' => StringHelper::nullableString((string) $this->readPath($payload, ['delivery.posted_at', 'shipping.posted_at'])),
'media_uuid' => null,
'payload_json' => null,
]];
@@ -900,7 +901,7 @@ final class ShopproOrdersSyncService
*/
private function mapNotes(array $payload): array
{
$comment = $this->nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment']));
$comment = StringHelper::nullableString((string) $this->readPath($payload, ['notes', 'comment', 'customer_comment']));
if ($comment === null) {
return [];
}
@@ -919,25 +920,6 @@ final class ShopproOrdersSyncService
return trim((string) $value);
}
private function normalizeDateTime(string $value): ?string
{
$trimmed = trim($value);
if ($trimmed === '') {
return null;
}
try {
return (new DateTimeImmutable($trimmed))->format('Y-m-d H:i:s');
} catch (Throwable) {
return null;
}
}
private function nullableString(string $value): ?string
{
$trimmed = trim($value);
return $trimmed === '' ? null : $trimmed;
}
private function normalizePaidFlag(mixed $value): bool
{
if ($value === true) {