Chore: Updating Box Now controllers, updates to product indexer for recommended products
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
namespace Modules\Core\Catalog\Recommendations;
|
||||
|
||||
use Illuminate\Support\Collection;
|
||||
use Lunar\Facades\ModelManifest;
|
||||
use Lunar\Models\Contracts\Product as ProductContract;
|
||||
use Lunar\Models\Product;
|
||||
use Modules\Core\Catalog\Contracts\RecommendationRule;
|
||||
|
||||
@@ -18,7 +20,14 @@ class RandomRule implements RecommendationRule
|
||||
{
|
||||
public function recommend(Product $product, int $limit, array $exclude): Collection
|
||||
{
|
||||
return Product::query()
|
||||
// ModelManifest::get(), not Product::query() directly — the base
|
||||
// Lunar\Models\Product has no custom_fields cast/fillable entry
|
||||
// (see Catalog\Models\Product's own docblock), so a recommendation
|
||||
// resolved as the base class silently lost that field once
|
||||
// ProductIndexer started reading it for recommendations.has_custom_fields.
|
||||
$model = ModelManifest::get(ProductContract::class);
|
||||
|
||||
return $model::query()
|
||||
->whereKeyNot($exclude)
|
||||
->inRandomOrder()
|
||||
->limit($limit)
|
||||
|
||||
@@ -189,6 +189,23 @@ class ProductIndexer extends BaseProductIndexer
|
||||
'name' => $recommendation->translateAttribute('name'),
|
||||
'price' => $this->cheapestPrice($recommendation, $currency),
|
||||
'image' => $recommendation->media->first() ? $this->mapMedia($recommendation->media->first())['thumb'] : null,
|
||||
// Same fields ProductCard::fromIndexed() (3dealer) reads off
|
||||
// a normal listing document to decide which button a card
|
||||
// shows at all — a recommendation with neither used to
|
||||
// render no button whatsoever, since it's built from this
|
||||
// embedded shape rather than a full ProductService document.
|
||||
// variant_id: same "first variant, no picker at card scope"
|
||||
// default every other listing card uses. custom_fields is
|
||||
// only readable at all because every RecommendationRule now
|
||||
// resolves products through ModelManifest (see Recommendations\
|
||||
// RandomRule) rather than the base Lunar\Models\Product
|
||||
// directly — that class has no custom_fields cast/fillable
|
||||
// entry (see Catalog\Models\Product's own docblock), so a
|
||||
// recommendation resolved as the base class would have
|
||||
// silently read null here regardless of the product's real
|
||||
// custom fields.
|
||||
'variant_id' => $recommendation->variants->first()?->id,
|
||||
'has_custom_fields' => ! empty($recommendation->custom_fields),
|
||||
])
|
||||
->all();
|
||||
|
||||
|
||||
@@ -180,6 +180,34 @@ class CheckoutTranslationsSeeder extends Seeder
|
||||
'page.confirmation_shipping_to' => ['Shipping to', 'Αποστολή σε'],
|
||||
'page.confirmation_billing' => ['Billing', 'Χρέωση'],
|
||||
'page.confirmation_continue' => ['Continue shopping', 'Συνέχεια αγορών'],
|
||||
'page.box_now_locker_label' => [
|
||||
'Choose a Box Now locker',
|
||||
'Επίλεξε Box Now locker',
|
||||
],
|
||||
'page.box_now_locker_loading' => [
|
||||
'Loading lockers…',
|
||||
'Φόρτωση lockers…',
|
||||
],
|
||||
'page.box_now_locker_required' => [
|
||||
'Choose a Box Now locker to continue.',
|
||||
'Επίλεξε ένα Box Now locker για να συνεχίσεις.',
|
||||
],
|
||||
'page.box_now_locker_select' => [
|
||||
'Select this locker',
|
||||
'Επιλογή αυτού του locker',
|
||||
],
|
||||
'page.box_now_locker_selected' => [
|
||||
'Selected',
|
||||
'Επιλέχθηκε',
|
||||
],
|
||||
'page.box_now_locker_search' => [
|
||||
'Search by area or address…',
|
||||
'Αναζήτηση με περιοχή ή διεύθυνση…',
|
||||
],
|
||||
'page.box_now_locker_no_results' => [
|
||||
'No lockers match your search.',
|
||||
'Δεν βρέθηκαν lockers για αυτή την αναζήτηση.',
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,14 @@ use Lunar\Models\Order;
|
||||
use Lunar\Models\State;
|
||||
use Modules\Core\Cart\Services\CartService;
|
||||
use Modules\Core\Checkout\Exceptions\InvalidShippingOptionException;
|
||||
use Modules\Core\Checkout\Exceptions\NoShippingAddressException;
|
||||
use Modules\Core\Checkout\Exceptions\TermsNotAcceptedException;
|
||||
use Modules\Core\Checkout\Exceptions\UnknownPaymentTypeException;
|
||||
use Modules\Core\Checkout\Services\CheckoutService;
|
||||
use Modules\Core\Customer\Services\CustomerAccountService;
|
||||
use Modules\Core\Payment\Enums\PaymentResultStatus;
|
||||
use Modules\Core\Payment\Models\PaymentMethod;
|
||||
use Modules\Core\Shipping\Carriers\BoxNow\BoxNowClient;
|
||||
|
||||
/**
|
||||
* The checkout page — one page, sections (contact / billing / shipping /
|
||||
@@ -280,6 +282,77 @@ class CheckoutController extends Controller
|
||||
return $this->fragments($cart, $options);
|
||||
}
|
||||
|
||||
/**
|
||||
* A plain, own-hosted stand-in for Box Now's Destination Map widget —
|
||||
* that widget only talks to their Production environment (see their
|
||||
* Partner API manual §4.1), which is useless while developing against
|
||||
* Stage credentials. Same underlying data (GET /destinations), no map.
|
||||
*/
|
||||
public function boxNowLockers(string $locale, BoxNowClient $boxNow): JsonResponse
|
||||
{
|
||||
$lockers = collect($boxNow->destinations())
|
||||
// Drops entries with a blank `name` (e.g. id 8288, "Virtual
|
||||
// Locker" in Sudan at lat 12.3/lng 25.3) — a real, in-range
|
||||
// coordinate, but sandbox test fixture noise rather than an
|
||||
// actual pickup point, and it alone was enough to make
|
||||
// fitBounds() below zoom the map out to the whole Balkans/
|
||||
// Middle East to fit every marker's cluster in Greece.
|
||||
->filter(fn (array $destination) => filled($destination['name'] ?? null))
|
||||
->map(fn (array $destination) => [
|
||||
'id' => $destination['id'],
|
||||
'name' => $destination['name'] ?? $destination['title'] ?? $destination['id'],
|
||||
'addressLine1' => $destination['addressLine1'] ?? null,
|
||||
'addressLine2' => $destination['addressLine2'] ?? null,
|
||||
'postalCode' => $destination['postalCode'] ?? null,
|
||||
'country' => $destination['country'] ?? null,
|
||||
'note' => $destination['note'] ?? null,
|
||||
'image' => $destination['image'] ?? null,
|
||||
'lat' => isset($destination['lat']) ? (float) $destination['lat'] : null,
|
||||
'lng' => isset($destination['lng']) ? (float) $destination['lng'] : null,
|
||||
])
|
||||
// Box Now's own Stage/sandbox data has at least one malformed
|
||||
// entry observed in practice (locker id 47: lat/lng as huge
|
||||
// integers with the decimal point apparently dropped, e.g.
|
||||
// 96065874308606 instead of ~37.96) — a single such point blows
|
||||
// out L.featureGroup().getBounds() on the frontend, zooming the
|
||||
// map out to near-nothing with every real marker imperceptible
|
||||
// at that scale. Valid latitude/longitude ranges are absolute,
|
||||
// not guesswork, so filtering on them is safe regardless of
|
||||
// what BoxNow's API does or doesn't fix upstream.
|
||||
->filter(fn (array $locker) => $locker['lat'] !== null && $locker['lng'] !== null
|
||||
&& abs($locker['lat']) <= 90 && abs($locker['lng']) <= 180)
|
||||
->values();
|
||||
|
||||
return response()->json(['lockers' => $lockers]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists the shopper's chosen locker (radio/select change, same
|
||||
* autosave shape as selectShippingOption()) via
|
||||
* CheckoutService::selectBoxNowLocker() onto the cart's shipping
|
||||
* address meta.
|
||||
*/
|
||||
public function selectBoxNowLocker(string $locale, Request $request): JsonResponse
|
||||
{
|
||||
$locationId = (string) $request->input('locker_id');
|
||||
|
||||
if ($locationId === '') {
|
||||
return response()->json(['errors' => ['locker_id' => __('checkout.page.box_now_locker_required')]], 422);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->checkout->selectBoxNowLocker([
|
||||
'locationId' => $locationId,
|
||||
'name' => (string) $request->input('locker_name'),
|
||||
'addressLine1' => (string) $request->input('locker_address'),
|
||||
]);
|
||||
} catch (NoShippingAddressException) {
|
||||
return response()->json(['errors' => ['locker_id' => __('checkout.page.box_now_locker_required')]], 422);
|
||||
}
|
||||
|
||||
return response()->json(['ok' => true]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Autosave-select a payment method (radio change). Persists it via
|
||||
* CheckoutService (which also records it on Cart::meta and re-snapshots
|
||||
|
||||
@@ -14,6 +14,7 @@ use Modules\Core\Checkout\Http\Controllers\CheckoutController;
|
||||
* Loaded from Providers\CheckoutModuleServiceProvider inside the `web`
|
||||
* middleware group.
|
||||
*/
|
||||
|
||||
Route::prefix('{locale}')
|
||||
->middleware('locale')
|
||||
->group(function () {
|
||||
@@ -27,6 +28,13 @@ Route::prefix('{locale}')
|
||||
Route::post('checkout/shipping-option', [CheckoutController::class, 'selectShippingOption'])
|
||||
->name('checkout.shipping-option.select');
|
||||
|
||||
Route::get('checkout/box-now/lockers', [CheckoutController::class, 'boxNowLockers'])
|
||||
->name('checkout.box-now.lockers');
|
||||
|
||||
Route::post('checkout/box-now/locker', [CheckoutController::class, 'selectBoxNowLocker'])
|
||||
->name('checkout.box-now.locker.select');
|
||||
|
||||
|
||||
Route::post('checkout/payment-method', [CheckoutController::class, 'selectPaymentMethod'])
|
||||
->name('checkout.payment-method.select');
|
||||
|
||||
|
||||
Reference in New Issue
Block a user