Stage 12 — Online Store: public catalog + cart + orders
Schema: - online_orders (token-tracked, status workflow, delivery method/fee) - online_order_items (price snapshot, fulfilled flag) - part_cross_refs (OEM/equivalent codes for search) - parts.is_published (shop visibility) Storefront (ShopController, tenant subdomain, /shop): - Catalog with search across name/article/brand/cross-refs, category + in-stock filters, live stock, white-label themed layout - Part detail page with cross-ref codes - VIN search → VinDecoder → guided catalog search - Session cart (per-tenant key), guest checkout, order confirmation page - Respects settings.shop.enabled (404 when off); tenant-guarded Part::searchPublished matches cross-ref articles via whereHas. Order notifications (ShopOrderNotifier, best-effort): - Staff: Web Push to active users - Customer: Telegram if phone matches a linked client Filament (tenant): - OnlineOrderResource under "Magazin" nav group, status workflow, items relation, "Onorează" action issues stock via WarehouseService (FIFO) - PartResource: is_published toggle + column + bulk publish/unpublish + CrossRefsRelationManager - Settings: shop section (enable, delivery methods, fee, free-over) - Landing page: shop button when enabled Tests (6 new): - catalog 404 when disabled; lists published only; cross-ref search; order placement (token + items + total); fulfill issues stock; cross-tenant token isolation Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Notifications;
|
||||
|
||||
use App\Models\Central\Company;
|
||||
use App\Models\Tenant\Client;
|
||||
use App\Models\Tenant\OnlineOrder;
|
||||
use App\Models\Tenant\User;
|
||||
|
||||
/**
|
||||
* Notifies staff of a new online order (Web Push) and confirms to the customer
|
||||
* via Telegram if they have a linked chat. Best-effort — never throws.
|
||||
*/
|
||||
class ShopOrderNotifier
|
||||
{
|
||||
public function __construct(
|
||||
private WebPushService $push,
|
||||
private TelegramService $telegram,
|
||||
) {
|
||||
}
|
||||
|
||||
public function placed(OnlineOrder $order): void
|
||||
{
|
||||
$company = Company::withoutGlobalScopes()->find($order->company_id);
|
||||
if (! $company) return;
|
||||
|
||||
// ── Staff: Web Push to active users of this tenant ──
|
||||
$title = 'Comandă nouă #' . $order->number;
|
||||
$body = $order->customer_name . ' · ' . number_format((float) $order->total, 2) . ' '
|
||||
. ($company->settings['currency'] ?? 'MDL');
|
||||
$url = '/app/resources/online-orders/' . $order->id . '/edit';
|
||||
|
||||
$userIds = User::where('status', 'active')->pluck('id');
|
||||
foreach ($userIds as $uid) {
|
||||
$this->push->sendToUser((int) $uid, $title, $body, $url, 'shop-order-' . $order->id);
|
||||
}
|
||||
|
||||
// ── Customer: Telegram if their phone is linked ──
|
||||
$needle = Client::normalizePhone($order->customer_phone);
|
||||
if ($needle) {
|
||||
$client = Client::whereNotNull('telegram_chat_id')
|
||||
->whereRaw(
|
||||
"REPLACE(REPLACE(REPLACE(REPLACE(phone, ' ', ''), '-', ''), '(', ''), ')', '') LIKE ?",
|
||||
['%' . substr($needle, -9) . '%']
|
||||
)
|
||||
->first();
|
||||
|
||||
if ($client && $client->telegram_chat_id) {
|
||||
$brand = htmlspecialchars($company->display_name ?? $company->name);
|
||||
$text = "🛒 <b>Comanda #{$order->number} primită</b>\n"
|
||||
. "Total: <b>" . number_format((float) $order->total, 2) . " "
|
||||
. ($company->settings['currency'] ?? 'MDL') . "</b>\n\n"
|
||||
. "Urmărește statusul: " . $order->trackingUrl() . "\n\n{$brand}";
|
||||
$this->telegram->sendMessage($company, (string) $client->telegram_chat_id, $text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user