OrderPickupPointApplier
in package
Applies the pickup point of an external carrier to an order.
Shared by the front end checkout (OrderCheckoutManager) and the admin order build
(OrderBuildTrait::getBuildData()). The build
rebuilds the shipper snapshot and would otherwise lose the pickup* columns, so both paths
call this one code.
IMPORTANT: reads only through raw DBAL (loadTariffContext()), never through Api find() or
findAll(). getBuildData() runs after the order graph is loaded and every Api read does
UnitOfWork::clear(), which would break the loaded graph (see the OrderBuildTrait docblock).
Table of Contents
Properties
- $carrierRegistry : CarrierRegistry
- $em : EntityManagerInterface
- $manualPickupPointFactory : ManualPickupPointFactory
Methods
- __construct() : mixed
- apply() : array{success: false, errorCode: string, message: string}|null
- Processes the choice of an external carrier pickup point: normalizes the raw widget payload through the carrier driver (sanitizing happens inside the driver) or the manual admin input, and writes the result to the 1:1 snapshot `OrderShipper.pickup*` and to the order delivery address (like `applyBranchToDeliveryAddress()` for own branches).
- applyFromPayload() : array{success: false, errorCode: string, message: string}|null
- Single entry of the admin path `api/eshop/order/build` ({@see \App\UI\Api\Eshop\Models\Managers\Traits\OrderBuildTrait::getBuildData()}): extracts the tariff context from the payload, applies the provenance gate ({@see selectionBelongsToTariff()}) and only then calls {@see apply()}. The decision whether a selection belongs to a tariff lives in a testable service and the payload reaches the applier in exactly one way.
- clearDeliveryAddressIfMatchesPickup() : void
- Counterpart of {@see applyToDeliveryAddress()} for a switch from pickup to non-pickup.
- loadTariffContext() : array{externalPickup: bool, shipperId: ?int, scriptName: ?string}|null
- Returns `external_pickup` of the tariff and the `script_name` of its carrier in one query.
- selectionBelongsToTariff() : bool
- Tells whether a pickup point selection belongs to the given tariff (selection provenance).
- applyToDeliveryAddress() : void
- Turns the chosen pickup point into the order delivery address, the same principle as `applyBranchToDeliveryAddress()` for own branches (the schema has no other field for it).
- manualFields() : array{code: mixed, name: mixed, street: mixed, city: mixed, zip: mixed, country: mixed}
- Takes the six manual fields; nothing else from the payload is taken over, and {@see \App\UI\Api\Eshop\Carriers\PickupPointSanitizerTrait::buildManualPickupPoint()} sanitizes them.
- pickupRequired() : array{success: false, errorCode: string, message: string}
- stringKeyed() : array<string, mixed>
- Narrows the payload to `array<string, mixed>` (not `array_filter(..., ARRAY_FILTER_USE_KEY)`): PHPStan cannot narrow `array<mixed, mixed>` from a bare `is_array()`, while a widget payload is always a JSON object (string keys).
Properties
$carrierRegistry read-only
private
CarrierRegistry
$carrierRegistry
$em read-only
private
EntityManagerInterface
$em
$manualPickupPointFactory read-only
private
ManualPickupPointFactory
$manualPickupPointFactory
Methods
__construct()
public
__construct(EntityManagerInterface $em, CarrierRegistry $carrierRegistry, ManualPickupPointFactory $manualPickupPointFactory) : mixed
Parameters
- $em : EntityManagerInterface
- $carrierRegistry : CarrierRegistry
- $manualPickupPointFactory : ManualPickupPointFactory
apply()
Processes the choice of an external carrier pickup point: normalizes the raw widget payload through the carrier driver (sanitizing happens inside the driver) or the manual admin input, and writes the result to the 1:1 snapshot `OrderShipper.pickup*` and to the order delivery address (like `applyBranchToDeliveryAddress()` for own branches).
public
apply(Order $order, array{externalPickup: bool, shipperId: ?int, scriptName: ?string}|null $tariffPickup, mixed $rawPickupPoint, mixed $manualFields, bool $persist) : array{success: false, errorCode: string, message: string}|null
The raw $rawPickupPoint (widget) takes precedence; $manualFields is used only when the
raw value is missing or the driver does not normalize it (null). The admin JS empties
the raw hidden field when fields are overwritten by hand, so the two paths do not cross.
Fail-closed:
- With
externalPickup=false(the tariff does not need it) both inputs are always ignored silently, even if a client sends them for another tariff: no write, no error. A stale pickup snapshot (a switch from another tariff, or an admin turning offexternal_pickupon the same tariff) is cleared;OrderSnapshotBuilderclears only when$tariffIdchanges, so this is a defensive second guard on the same snapshot. - With
externalPickup=trueand no valid PickupPoint obtainable (missing payload, missingShipper::$scriptName, no driver in CarrierRegistry, or normalization returnsnull) all cases yield the same single errorpickupPointRequired. - A carrier without a driver does not block manual input: the admin must be able to type
an address where no widget exists; the snapshot carrier is then
ManualPickupPointFactory::CARRIER (
'manual'). - A preview (
$persist === false) returns no error, because the customer or admin is still choosing.
Parameters
- $order : Order
- $tariffPickup : array{externalPickup: bool, shipperId: ?int, scriptName: ?string}|null
- $rawPickupPoint : mixed
-
raw widget object (payload
pickupPoint) - $manualFields : mixed
-
manual admin fields (payload
pickupPointManual); the front end checkout always passesnullhere (customers have no manual path) - $persist : bool
Return values
array{success: false, errorCode: string, message: string}|null —null = OK
(either the point was written or the tariff has no pickup)
applyFromPayload()
Single entry of the admin path `api/eshop/order/build` ({@see \App\UI\Api\Eshop\Models\Managers\Traits\OrderBuildTrait::getBuildData()}): extracts the tariff context from the payload, applies the provenance gate ({@see selectionBelongsToTariff()}) and only then calls {@see apply()}. The decision whether a selection belongs to a tariff lives in a testable service and the payload reaches the applier in exactly one way.
public
applyFromPayload(Order $order, int $tariffId, array<string, mixed> $payload, bool $persist) : array{success: false, errorCode: string, message: string}|null
Parameters
- $order : Order
- $tariffId : int
- $payload : array<string, mixed>
-
the whole build payload (reads
pickupPoint,pickupPointManual,pickupPointTariffId) - $persist : bool
Return values
array{success: false, errorCode: string, message: string}|null —null = OK
clearDeliveryAddressIfMatchesPickup()
Counterpart of {@see applyToDeliveryAddress()} for a switch from pickup to non-pickup.
public
static clearDeliveryAddressIfMatchesPickup(Order $order, OrderShipper $snapshot) : void
Reconstructs what that method wrote for the last saved point (the same street/city
fallback to name as for Balikovna) and clears customerDlv* only on an exact match of
all fields; a single difference (the admin edited the address) means it is no longer the
pickup address and it stays untouched.
public static (no service state is used). Also called from
OrderSnapshotBuilder::buildShipper() and
buildShipperFromRow(), where clearPickupPoint() runs on a tariff change, before apply()
is reached and the snapshot would already be empty.
IMPORTANT: always call before $snapshot->clearPickupPoint(), which erases the old
pickup* values this method needs.
Parameters
- $order : Order
- $snapshot : OrderShipper
loadTariffContext()
Returns `external_pickup` of the tariff and the `script_name` of its carrier in one query.
public
loadTariffContext(int $tariffId) : array{externalPickup: bool, shipperId: ?int, scriptName: ?string}|null
Called after OrderSnapshotBuilder::buildShipper() or buildShipperFromRow(), which
already verified that $tariffId exists; a null return is safe and is not assumed to
mean an invalid tariff.
Parameters
- $tariffId : int
Return values
array{externalPickup: bool, shipperId: ?int, scriptName: ?string}|nullselectionBelongsToTariff()
Tells whether a pickup point selection belongs to the given tariff (selection provenance).
public
static selectionBelongsToTariff(bool $provenanceSent, int|null $selectionTariffId, int $tariffId) : bool
Problem: the admin form keeps the chosen point in two persistent controls (a hidden
pickupPoint with the raw widget object and six pickupPoint* text fields). When the
admin switches the tariff, the JS force-submits shippingRefresh and both sets go to the
server with the old values. OrderSnapshotBuilder::buildShipperFromRow() clears the
snapshot on a tariff change, but apply() runs after it and refills it from the same
payload: for another tariff of the same carrier the old raw value passes the driver
normalization, for another carrier the old six fields pass the manual branch. The result
is a pickup code of carrier A stored under carrier B without any new selection.
The server cannot tell this on its own: the previous tariff is read from the DB, but
nothing is saved until the final submit, so even after a legitimate new selection the DB
still holds the original tariff. Only the client knows, hence it sends
pickupPointTariffId, the tariff the selection was made for.
Contract: when the key is in the payload, it must match strictly (a mismatch discards both
inputs as if nothing was chosen, so persist=true fails closed with pickupPointRequired).
When the key is absent the caller does not take part in provenance (the front end
checkout OrderCheckoutManager always sends the
selection from the current step) and behavior is unchanged. A mere recalculation or save
of an unchanged tariff passes (the hidden field carries the same tariff), so a valid point
is never lost.
Parameters
- $provenanceSent : bool
- $selectionTariffId : int|null
- $tariffId : int
Return values
boolapplyToDeliveryAddress()
Turns the chosen pickup point into the order delivery address, the same principle as `applyBranchToDeliveryAddress()` for own branches (the schema has no other field for it).
private
applyToDeliveryAddress(Order $order, PickupPoint $point) : void
IMPORTANT: Balikovna deliberately does not map street/city
(see BalikovnaCarrier::normalizePickupPoint()): the
Czech Post manual forbids using the unstructured point.address on a label, because it may
carry a different postcode than the real point.zip. To keep the delivery address from
staying half empty, a missing street/city is filled from PickupPoint::$name; zip is
never filled from anything but PickupPoint::$zip.
Parameters
- $order : Order
- $point : PickupPoint
manualFields()
Takes the six manual fields; nothing else from the payload is taken over, and {@see \App\UI\Api\Eshop\Carriers\PickupPointSanitizerTrait::buildManualPickupPoint()} sanitizes them.
private
manualFields(array<string|int, mixed> $value) : array{code: mixed, name: mixed, street: mixed, city: mixed, zip: mixed, country: mixed}
Parameters
- $value : array<string|int, mixed>
Return values
array{code: mixed, name: mixed, street: mixed, city: mixed, zip: mixed, country: mixed}pickupRequired()
private
pickupRequired() : array{success: false, errorCode: string, message: string}
Return values
array{success: false, errorCode: string, message: string}stringKeyed()
Narrows the payload to `array<string, mixed>` (not `array_filter(..., ARRAY_FILTER_USE_KEY)`): PHPStan cannot narrow `array<mixed, mixed>` from a bare `is_array()`, while a widget payload is always a JSON object (string keys).
private
stringKeyed(array<string|int, mixed> $value) : array<string, mixed>
Parameters
- $value : array<string|int, mixed>