ArtisanPack UI
DocsBookingsTroubleshooting

Troubleshooting

Bookings v1.0.0

Behind a load balancer or CDN without trusted proxies configured, returns the proxy's address, so every customer shares one bucket and the fifth booking...

Every customer hits the rate limit at once

Behind a load balancer or CDN without trusted proxies configured, Request::ip() returns the proxy's address, so every customer shares one post bucket and the fifth booking of the minute is refused for all of them. Configure Laravel's trusted proxies in bootstrap/app.php. See Trusted Proxies.

The booking widget doesn't appear

The widget is a Livewire component, registered only when livewire/livewire is installed:

composer require livewire/livewire

If it still doesn't render, confirm your layout includes @livewireStyles / @livewireScripts (or @livewire directives per your setup).

Frontend change isn't showing

Rebuild assets:

npm run build   # or: npm run dev

SlotUnavailableException on create

create() throws this only when nobody at all could take the slot — the provider is fully booked, outside their schedule, or on a blackout date at that time. Check the provider's AvailabilitySchedule, existing bookings, and any ServiceBlackoutDate. See Creating a Booking.

Two bookings landed on the same slot

This should not happen with MySQL or PostgreSQL, which use server-side advisory locks. On SQLite (or any engine without them) the lock falls back to the cache store — exclusive within one app server only. If you run more than one, point artisanpack.bookings.lock.store at a shared store like redis. See Requirements.

A webhook delivery is marked dead or the endpoint disabled

A delivery is retried on webhooks.delivery_backoff_minutes (six attempts over ~14 hours) before it is marked dead. An endpoint is disabled after webhooks.failure_threshold consecutive failures — and a single success resets the count. Disabling fires the WebhookDisabled event. Check the reason on the booking_webhook_deliveries row. See Outbound Webhooks.

A webhook to an internal host is refused

The SSRF guard refuses any URL whose host resolves to a private, loopback, link-local, or unique-local address, at delivery time. To deliver to an internal host on purpose, add it to webhooks.url_guard.allowed_hosts. See Webhook Security.

A calendar sweep reports "nothing synced"

The calendar sweeps find what is due and then need a CalendarSyncDriver to act on it. Google ships here (gated on artisanpack-ui/google); Microsoft and Apple ship in their companion packages. Until a driver is installed and enabled, the sweeps report what they found and warn that nothing was synced. See Calendar Sync.

A subscribed iCal feed stopped updating

A feed 404s silently after its token is rotated. Rotating a provider's token (running bookings:ical-token again) or reissuing manage tokens invalidates the old subscription URLs — every subscribed client then has to be given the new URL. See iCal Feeds.

SMS "sends" but nothing arrives

The default sms_driver is null, which logs the message and sends nothing. Set notifications.sms_driver to a real gateway class and add sms to notifications.channels. A driver name that resolves to nothing throws rather than silently falling back. See Text Messages.

Reminders sent twice

The notification log is what stops a reminder being sent twice. If you pruned booking_notification_log inside a reminder's own window, the claim was removed and the reminder can fire again. Keep retention.notification_log_days longer than your longest reminder in hours_before. See Reminders.

Staff database notifications go missing

The laravel database driver expects Laravel's own notification storage (a UUID key, a JSON data column). artisanpack-ui/cms-framework ships an incompatible notifications table, so under both, point notifications.database.notifiable at storage of your own — or use the cms driver. A failed write is logged, not thrown, so the customer's email still goes out. See CMS Framework.

Running the tests

composer test            # everything that runs on in-memory SQLite
composer test:mysql      # the mysql group, against a real MySQL server
composer test:postgres   # the postgres group, against a real Postgres server

A few race-safety tests need a real MySQL/Postgres advisory lock and skip when the server is unreachable — so a plain composer test is green without one. CI sets BOOKINGS_REQUIRE_EXTERNAL_DB=1 to turn that skip into a failure, since a skipped lock test reads as "race-safety verified" while verifying nothing.

Code style

Both formatters come from artisanpack-ui/code-style-pint. Run them together — Pint first, then PHP-CS-Fixer for the WordPress-style spacing Pint cannot produce:

composer fix    # pint, then php-cs-fixer
composer lint   # php-cs-fixer --dry-run + pint --test + phpcs