Skip to content

Quotations — Developer Guide

Mirror of the Opportunities developer guide (assignee scope, notes, domain timeline), kept leaner: no pipeline/board, no related Contact/Company/Lead FKs beyond the required Opportunity link. Prefer copying those patterns over inventing new ones.

Backend layout

PiecePath
Modelsapp/Models/Quotation.php, QuotationLine, QuotationNote, QuotationActivity
EnumsQuotationStatusEnum, QuotationActivityTypeEnum, DocumentDiscountTypeEnum
Supportapp/Support/Billing/DocumentTotalsCalculator.php, DocumentDiscountRules.php, DocumentHtmlSanitizer.php, QuotationInvoiceGuard.php
PDFapp/Services/Tenant/QuotationPdfService.php, resources/views/quotations/pdf.blade.php
Serviceapp/Services/Tenant/QuotationService.php (+ ScopesToAssignee)
Controllerapp/Http/Controllers/Tenant/Api/V1/QuotationController.php
Requestsapp/Http/Requests/Tenant/Api/V1/Quotation/*
Resourcesapp/Http/Resources/Tenant/Api/V1/Quotation/*
Policyapp/Policies/QuotationPolicy.php
Eventsapp/Events/Quotation*.php
Subscriberapp/Listeners/QuotationEventSubscriber.php (audit + assignment notification)
Notificationsapp/Notifications/Tenant/Quotation/QuotationAssignedNotification.php
Link rulesLinkableContact, LinkableCompanyForOpportunity, EligibleOpportunityAssignee
Dependency migrationdatabase/migrations/2026_07_30_210006_add_quotations_opportunities_dependency.php (mirrors 2026_07_22_000003_add_meetings_calendar_dependency.php)
Teststests/Feature/Tenant/Quotation/QuotationTest.php, tests/Feature/Central/Module/QuotationsModuleDependencyTest.php

Domain notes

  • Hard dependency: Quotations declares a required module_dependencies row on Opportunities — Marketplace install is blocked until Opportunities is entitled (unlike Opportunities' own soft-only related FKs).
  • Status machine lives on QuotationStatusEnum::allowedTransitions() / canTransitionTo(): draft → sent → accepted|rejected|expired. QuotationService::transitionStatus() throws ValidationException (422, status field) for disallowed transitions, including re-entering the same status (e.g. sending an already-sent quotation).
  • Content updates (PUT) and line sync are draft-only via Quotation::isEditable(). Assignment remains available after send via POST …/assign.
  • POST …/status maps target status to permissions: sentquotations.send, acceptedquotations.accept, otherwise quotations.update.
  • send / accept / convert policies are assignee-scoped (same as view / update) unless the actor has quotations.assign or is superadmin.
  • Convert to invoice (QuotationService::convertToInvoice()): soft Invoices entitlement (422 if not installed). One-shot via QuotationInvoiceGuard (any invoice with this quotation_id, including soft-deleted, estimate converts, or contract bills) inside a transaction with lockForUpdate on the quotation. Soft-deleted invoices still block; errors tell ops to restore or force-delete. Copies lines into a draft CustomerInvoice, sets quotation_id, auto-accepts if sent, records converted, fires QuotationConverted.
  • Send is status-only — no outbound email delivery on POST …/send; PDF download is available separately via Download PDF.
  • Customer email: POST …/email (quotations.send, assignee-scoped, throttle:billing-document-email) delivers a branded message with optional PDF attachment via BillingDocumentMailer + QuotationEmailService. Requires a sent quotation (sent/accepted/rejected/expired; draft → 422). For Sent quotes, rotates an acceptance token and appends FrontendUrl::quotationAccept(). Records QuotationActivityTypeEnum::Emailed and tenant email logs (quotation.emailed).
  • Customer e-signature v1: POST …/acceptance-link (quotations.send) rotates token hash + expiry. Public GET/POST /public/quotations/accept/{token} (throttle quotation-acceptance) shows summary / accepts with signer name+email+IP. Columns: acceptance_token_hash, acceptance_token_expires_at, accepted_at, accepted_by_name, accepted_by_email, acceptance_ip. Activity types signature_requested / signed. SPA guest page /#/accept/quotations/:token.
  • Line items are fully replaced on create/update (QuotationService::syncLines()); Quotation::recalculateTotals() delegates to DocumentTotalsCalculator for subtotal / discount_total / tax_total / total from persisted QuotationLine rows plus document line_discount_type. Tax is calculated after line discounts.
  • Lines use required short name plus optional long body, optional product_id (LinkableProduct: Products entitled, products.view or superadmin, active non-trashed). Memo notes accept sanitized HTML via DocumentHtmlSanitizer.
  • Shared line discounts use DocumentDiscountTypeEnum (none, percent, fixed) on the parent as line_discount_type; lines store only discount_value. Validation lives in DocumentDiscountRules.
  • PDF: GET …/pdf (quotations.view, assignee-scoped, throttle:quotations-pdf) renders from resources/views/quotations/pdf.blade.php via QuotationPdfService — same branded layout as invoices. Totals are right-aligned; Notes / Terms are full-width blocks below (not a side-by-side table cell) so Dompdf paginates long HTML across all pages. Line-item body HTML is a block-level .line-body under each short pricing row (not a tall table cell).
  • Assignee scoping via ScopesToAssignee with quotations.assign.
  • quotations.force.delete is not granted to any default role — owner/superadmin only.
  • contact_id / company_id are optional and validated for module entitlement + assignee scope, same as Opportunities.

Permissions

quotations.view | create | update | delete | restore | force.delete | assign | send | accept | convert

Routes use module:quotations then can:quotations.* / policies.

Catalog: slug quotations, category sales, is_default_included = false, is_billable = false, sort_order = 50, version 1.9.0. Registered via DefaultModuleRegistrar migration (migrate-only); 1.3.0 added optional product line picker; 1.3.1 hardens linking + sanitizer; 1.4.0 adds convert-to-invoice (soft Invoices entitlement); 1.4.1 adds convert row locks + soft-delete recovery messaging; 1.5.0 dedicated record pages; 1.5.1 PDF long-notes pagination; 1.5.2 PDF long line-body pagination; 1.6.0 customer email delivery (POST …/email); 1.8.0 Automation triggers; 1.9.0 customer e-signature accept links.

API (tenant)

Base: /api/tenant/v1 — full reference tenant-v1-quotations.md.

Frontend

SPA should mirror Opportunities (table + create/edit page, record page) under the existing AppLayout — do not invent a parallel shell.

PiecePath (expected)
Pagesrc/pages/quotations/
Form / detailcreate/edit page + record page (Overview, Lines, Notes, Activity) — shared DocumentLinesEditor, DocumentTotalsPanel, RichTextEditor for memo notes
ServicequotationService in src/api/services.ts
Navpermission: quotations.view, module: 'quotations' (Sales)
Playwrighte2e/pages/quotations.page.ts, e2e/tests/quotations/, npm run test:e2e:quotations

Tests

bash
php artisan test --compact tests/Feature/Tenant/Quotation/QuotationTest.php tests/Feature/Central/Module/QuotationsModuleDependencyTest.php
npm run typecheck && npm run lint && npm run build
npm run test:e2e:quotations

Logging

  • Spatie LogsActivity on Quotation (log name quotations)
  • Domain quotation_activities timeline
  • PlatformAuditService via QuotationEventSubscriber

Ask EloSync

Ask EloSync Quotation tools (get_quotation, confirmed status/assign/note writes) are registered in AIToolRegistry and confirmed via PendingAiActionService. Status auth mirrors HTTP POST …/status (Sent→send, Accepted→accept, else update) via QuotationAiSupport::authorizeStatusChange and QuotationService::changeStatus. Assign uses EligibleOpportunityAssignee. See AI tools and AI Quotation triage production readiness.

Official documentation for the EloSync SaaS Platform.