Repository navigation
[Quotes]: Send quotes via email for client approval #164
Copy link
Copy link
Closed
Labels
refinedRefined with SMART plan / test suggestionsRefined with SMART plan / test suggestions
Milestone
Description
SMART Plan — [Quotes] Send a quote via email for client approval
Specific
The existing "send email" stub action in QuotesTable is implemented to resolve the prospect's primary contact email via Relation → Contact → Communications, render the email body using a quote-type EmailTemplate, dispatch a queued QuoteMailable, and update the Quote status to Sent. A Filament success or error notification is shown after dispatch.
Measurable
- Calling the "send email" action dispatches exactly one queued
QuoteMailableaddressed to the prospect's primary contact email. - The
Quoterecord'sstatuscolumn isSentin the database after a successful dispatch. - An error Filament notification is shown (and status does not change) when no contact email can be resolved for the prospect.
Achievable
- Service method — implement
sendQuoteEmail(Quote $quote): voidinModules/Quotes/Services/QuoteService.php; resolve recipient via$quote->prospect->contacts->first()?->communications->where('type', 'email')->first()?->value; throw aRuntimeExceptionif null. - EmailTemplate lookup — query
EmailTemplatefortype = 'quote'scoped to the current company; fall back to default subject/body. - Mailable — create
Modules/Quotes/Mail/QuoteMailable.php(implementsShouldQueue); acceptQuoteand template data; render body string into the mail view. - Dispatch & status update —
Mail::to($email)->queue(new QuoteMailable($quote, $template))then$quote->update(['status' => QuoteStatus::Sent]), both inside the service method. - Action wiring — replace the stub
->action()closure inModules/Quotes/Filament/Company/Resources/Quotes/Tables/QuotesTable.phpto callQuoteService::sendQuoteEmail($record), wrap in try/catch, fireNotification.
Relevant
Sending quotes for approval directly from the app closes the loop between quoting and customer sign-off without requiring external email clients.
Time-bound
Sprint 5.
Arrange · Act · Assert
// Modules/Quotes/Tests/Feature/QuotesTest.php
#[Test]
public function it_dispatches_a_queued_mail_and_marks_quote_sent_when_send_email_action_is_called(): void
{
Mail::fake();
/* Arrange */
$relation = \Modules\Clients\Models\Relation::factory()
->has(
\Modules\Clients\Models\Contact::factory()
->has(
\Modules\Clients\Models\Communication::factory()
->state(['type' => 'email', 'value' => 'prospect@example.com']),
'communications'
),
'contacts'
)
->for($this->company)
->create();
$quote = \Modules\Quotes\Models\Quote::factory()
->for($this->company)
->for($relation, 'prospect')
->create(['status' => \Modules\Quotes\Enums\QuoteStatus::Draft]);
/* Act */
$component = Livewire::actingAs($this->user)
->test(\Modules\Quotes\Filament\Company\Resources\Quotes\Pages\ListQuotes::class)
->mountAction(TestAction::make('send-email')->table($quote))
->callMountedAction();
/* Assert */
$component->assertSuccessful();
Mail::assertQueued(
\Modules\Quotes\Mail\QuoteMailable::class,
fn ($mail) => $mail->hasTo('prospect@example.com')
);
$this->assertDatabaseHas('quotes', [
'id' => $quote->id,
'status' => \Modules\Quotes\Enums\QuoteStatus::Sent->value,
]);
}Reacted by coderabbitai
Metadata
Metadata
Assignees
Labels
refinedRefined with SMART plan / test suggestionsRefined with SMART plan / test suggestions
Activity
coderabbitai commentedon Dec 28, 2025
📝 CodeRabbit Plan Mode
Generate an implementation plan and prompts that you can use with your favorite coding agent.
Examples
🔗 Similar Issues
Related Issues
👤 Suggested Assignees
🧪 Issue enrichment is currently in open beta.
You can configure auto-planning by selecting labels in the issue_enrichment configuration.
To disable automatic issue enrichment, add the following to your
.coderabbit.yaml:💬 Have feedback or questions? Drop into our discord!
[-][Quotes] As a user_admin, I want to send quotes via email for client approval.[/-][+][Quotes]: Send quotes via email for client approval[/+]nielsdrost7 commentedon Jul 18, 2026
SMART Plan — [Quotes] Send a quote via email for client approval
Specific
The "send email" stub in
Modules/Quotes/Filament/Company/Resources/Quotes/Tables/QuotesTable.php:92-98becomes a modal action mirroring the invoice pipeline (EmailInvoiceAction+InvoiceService::resolveEmailDefaults/sendInvoiceEmail):Relation → Contact → Communications(prefer primary email), subject/body rendered from the company'squote-typeEmailTemplate(seeded titlequote_sent); all fields editable before send (v1 compose viewmailer/views/quote.php).Modules/Quotes/Mail/QuoteMailable, PDF of the quote attached (sameemail_pdf_attachmentsetting as [Invoices] Attach invoice PDF automatically when sending an invoice email #526), quote uploads attached.Senton success (v1Mdl_quotes::mark_sent), quote number generated if missing,MailQueuerow logged via the quote'smailQueue()morph (pattern from [Invoices] Create/update MailQueue record to log email delivery (timestamp, recipient, status) #527).InvoicesTable.php:198-203); failure shows error notification and changes nothing ([Invoices] Handle invoice email send failures gracefully — show error notification, no status change #531 pattern).Measurable
QuoteMailableto the prospect's email with PDF attachment.quotes.status = sentafter success; unchanged on failure.mail_queuerow exists for the quote.Achievable
QuoteService::resolveEmailDefaults(Quote)+QuoteService::sendQuoteEmail(Quote, string $recipient, string $subject, string $body)— port the invoice implementations;EmailTemplateVariableResolveralready acceptsInvoice|Quote.Modules/Quotes/Mail/QuoteMailable.php(ShouldQueue), PDF via the quotes PDF pipeline ([Quotes]: Download the PDF for a quote from the quote list #196).EmailDocumentActionif trivial; otherwise mirrorEmailInvoiceActionasEmailQuoteActionand register it inQuotesTableandEditQuote.EmailTemplateTypeenum fix (invoice/quote/payment/reminder) to filter templates by type.Relevant
Closes the quoting loop in-app; parent of the quotes action-group completion #35. #199 was consolidated here.
Time-Bound
Sprint after the invoice email sub-tasks (#526/#527/#529/#531) land, reusing their plumbing.
Arrange
Mail::fake(); Relation with contact + email communicationprospect@example.com; Draft quote for that relation; company panel test case.Act
Mount the
send emailtable action onListQuotesfor the quote, submit with defaults.Assert
Mail::assertQueued(QuoteMailable::class, fn ($m) => $m->hasTo('prospect@example.com'))quotes.status=sentmail_queuerow exists withmailable_type= Quote morphdraftnielsdrost7 commentedon Jul 24, 2026
Test Suggestions
Added to support future implementation of this request. The issue body contains a complete SMART Plan.
The body already frames this as a concrete feature: dispatch a
QuoteMailable, resolve the recipient email via the relation's primary contact, and update the quote status toSent. Here are additional edge cases worth covering in PHPUnit:it_shows_an_error_when_no_contact_email_is_found_on_the_prospect()it_uses_the_company_email_template_when_looking_up_the_quote_template()feat: Send quotes via email for client approval. Closes InvoicePlane#164
feat: Send quotes via email for client approval. Closes #164
feat: Send quotes via email for client approval. Closes #164