Configuration and routes
Reference for the odden-sales config file, public routes, Artisan commands, scheduling, views, and events.
This page lists everything you can configure in getodden/crm-sales, plus the routes, commands, and events it registers. For settings shared by every Odden package, such as the user model and Core's rate limits, see Configuration.
Publishing
php artisan vendor:publish --tag=odden-sales-config # config/odden-sales.php
php artisan vendor:publish --tag=odden-sales-migrations # copies migrations into database/migrationsMigrations are loaded from the package automatically, so publishing them is only needed if you want to edit them. Published copies keep their filenames, so Laravel treats them as the same migrations and doesn't run them twice.
Config reference
tables
Table names for every sales model. Change them before running the migrations.
| Key | Default |
|---|---|
odden-sales.tables.pipelines |
odden_pipelines |
odden-sales.tables.stages |
odden_pipeline_stages |
odden-sales.tables.deals |
odden_deals |
odden-sales.tables.stage_history |
odden_deal_stage_history |
odden-sales.tables.products |
odden_deal_products |
odden-sales.tables.quotes |
odden_quotes |
odden-sales.tables.quote_items |
odden_quote_items |
odden-sales.tables.automations |
odden_stage_automations |
odden-sales.tables.quotas |
odden_sales_quotas |
odden-sales.tables.email_templates |
odden_sales_email_templates |
odden-sales.tables.sequences |
odden_sales_sequences |
odden-sales.tables.sequence_enrollments |
odden_sales_sequence_enrollments |
odden-sales.tables.playbooks |
odden_sales_playbooks |
odden-sales.tables.meeting_links |
odden_sales_meeting_links |
odden-sales.tables.meeting_bookings |
odden_sales_meeting_bookings |
odden-sales.tables.lead_routing_rules |
odden_sales_lead_routing_rules |
default_currency
'default_currency' => env('ODDEN_DEFAULT_CURRENCY', 'USD'),The package does not currently read this value. New deals, quotas, and quotes take their currency from the database column default (USD) or from what you pass. Generated quotes copy the deal's currency.
mail
'mail' => [
'mailer' => env('ODDEN_SALES_MAILER'),
'connection' => env('ODDEN_SALES_QUEUE_CONNECTION'),
'queue' => env('ODDEN_SALES_MAIL_QUEUE'),
'from' => [
'address' => env('ODDEN_SALES_FROM_ADDRESS'),
'name' => env('ODDEN_SALES_FROM_NAME'),
],
'sequences' => [
'send_as_owner' => (bool) env('ODDEN_SALES_SEND_AS_OWNER', false),
],
],Sales sends two kinds of email: sequence email steps (Odden\Sales\Mail\SequenceStepMail) and meeting confirmations (Odden\Sales\Mail\MeetingBookedMail). Both implement ShouldQueue and are dispatched after the surrounding database transaction commits, so nothing is sent during the web request or command. Run a queue worker (php artisan queue:work, adding --queue= if you set a queue name); with the sync queue connection, mail is sent immediately instead.
| Key | Env | Default | Effect |
|---|---|---|---|
mail.mailer |
ODDEN_SALES_MAILER |
null |
Mailer from config/mail.php to send with. null uses the default mailer. |
mail.connection |
ODDEN_SALES_QUEUE_CONNECTION |
null |
Queue connection. null uses the default connection. |
mail.queue |
ODDEN_SALES_MAIL_QUEUE |
null |
Queue name. null uses the connection's default queue. |
mail.from.address |
ODDEN_SALES_FROM_ADDRESS |
null |
From address for Sales email. null uses your app's mail.from. |
mail.from.name |
ODDEN_SALES_FROM_NAME |
null |
From name, used with mail.from.address. |
mail.sequences.send_as_owner |
ODDEN_SALES_SEND_AS_OWNER |
false |
Send sequence emails from the enrollment owner's address and name. When false the owner is only the reply-to. |
meetings
'meetings' => [
'default_working_hours' => [
'monday' => ['09:00-17:00'],
'tuesday' => ['09:00-17:00'],
'wednesday' => ['09:00-17:00'],
'thursday' => ['09:00-17:00'],
'friday' => ['09:00-17:00'],
],
'booking_window_days' => (int) env('ODDEN_SALES_BOOKING_WINDOW_DAYS', 60),
],| Key | Env | Default | Effect |
|---|---|---|---|
meetings.default_working_hours |
Monday to Friday, 09:00-17:00 |
Used by meeting links whose working_hours is empty. Same shape as working hours, in each link's timezone. |
|
meetings.booking_window_days |
ODDEN_SALES_BOOKING_WINDOW_DAYS |
60 |
How many days ahead visitors can book. |
routes
'routes' => [
'enabled' => (bool) env('ODDEN_SALES_ROUTES_ENABLED', true),
'web' => [
'domain' => env('ODDEN_SALES_DOMAIN'),
'prefix' => env('ODDEN_SALES_PREFIX', ''),
'middleware' => ['web'],
],
],| Key | Env | Default | Effect |
|---|---|---|---|
routes.enabled |
ODDEN_SALES_ROUTES_ENABLED |
true |
Set to false to skip registering the public routes. |
routes.web.domain |
ODDEN_SALES_DOMAIN |
null |
Serve the routes on one domain, for example deals.example.com. |
routes.web.prefix |
ODDEN_SALES_PREFIX |
'' |
URL prefix, for example sales gives /sales/quotes/{token}. |
routes.web.middleware |
['web'] |
Middleware for the route group. Keep web: the forms need sessions and CSRF protection. |
Empty values are dropped, so a null domain or empty prefix applies no constraint.
ODDEN_SALES_DOMAIN=proposals.example.com
ODDEN_SALES_PREFIX=Public routes
These routes have no authentication; access is by quote token or meeting slug.
| Method | URI | Name | Purpose |
|---|---|---|---|
| GET | /quotes/{token} |
odden.quotes.show |
Customer quote page. See Quotes. |
| POST | /quotes/{token}/accept |
odden.quotes.accept |
Accept and sign a quote. |
| GET | /meet/{slug} |
odden.meetings.show |
Meeting booking page. See Meeting links. |
| POST | /meet/{slug}/book |
odden.meetings.book |
Book a meeting. |
The two POST routes use the throttle:odden-public middleware. Core defines that limiter: odden-core.rate_limits.public requests per minute per IP, default 30 (ODDEN_PUBLIC_RATE_LIMIT).
If you set routes.enabled to false and register your own routes, keep the four route names. The package's views and controllers generate URLs and redirects with them.
Views
Views are registered under the odden-sales namespace:
| View | Used by |
|---|---|
odden-sales::quotes.public-portal |
odden.quotes.show |
odden-sales::meetings.book |
odden.meetings.show |
odden-sales::mail.meeting-booked |
Body of the meeting confirmation email. |
odden-sales::deals.health-score-modal |
Not used by a route. Takes a $health array and requires Filament. See Health score. |
There is no publish tag for views. To override one, create a file with the same path under resources/views/vendor/odden-sales/ in your app.
Artisan commands
| Command | What it does |
|---|---|
sales:process-cadences |
Runs due sequence steps, queuing email steps for delivery, and prints a summary table. See Processing due steps. |
sales:expire-quotes |
Sets draft, sent, and approved quotes whose expires_at is before today to expired. See Expiring stale quotes. |
Neither command takes arguments or options.
Scheduling
The package does not add anything to the scheduler. Sequences don't advance and quotes don't expire until you schedule the commands, for example in routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('sales:process-cadences')->hourly();
Schedule::command('sales:expire-quotes')->daily();Sequence steps are due by date, so running sales:process-cadences more than once a day only matters for picking up manual steps that reps have completed. Running it again never sends the same email step twice.
Sequence emails and meeting confirmations are queued, so a queue worker must also be running for them to be delivered. See mail.
Events
| Event | Dispatched when |
|---|---|
Odden\Sales\Events\DealMovedStage |
A deal changes stage. |
Odden\Sales\Events\DealWon |
A deal enters a closed won stage. |
Odden\Sales\Events\DealLost |
A deal enters a closed lost stage. |
Properties and timing are described in Deals. The package registers no listeners for them.
Side effects at a glance
The only mail Sales sends is sequence email steps and meeting confirmations, and both are queued (see mail). These are the writes that happen in the background of a call:
| Trigger | Side effect |
|---|---|
| Deal enters a stage | Stage automations run; stage history row written; events dispatched; on won/lost, associated contacts' active sequence enrollments are unenrolled. |
DealProduct saved, deleted, or restored |
Deal amount recalculated. |
QuoteItem saved or deleted |
Quote subtotal and total_amount recalculated. |
| Quote page viewed | Draft quote becomes sent; a view note is logged on the deal (at most every two hours). |
| Quote accepted | Deal moved to closed won, amount set from the quote, note and owner task logged. |
| Lead routed | owner_id updated; note logged. |
| Contact enrolled in a sequence | New lead status becomes InProgress. |
| Meeting booked | Slot re-checked under a lock; contact created or matched; meeting activity logged; active sequence enrollments unenrolled; booking recorded; confirmation emails with an .ics invite queued to the visitor and the rep. |
| Sequence email step processed | Email queued to the contact (or the step skipped, with the reason logged); email activity logged; contact marked contacted. |