Skip to main content
The Quote Builder is an unauthenticated, single-page experience served at GET /cotizador. It loads the quotes.builder Blade view, which bootstraps the frontend SPA. The page fetches the block catalogue from the API, lets the client assemble a quote interactively, and submits the result — all without requiring a login.

How it works

1

Page load

The browser requests GET /cotizador. The server renders quotes.builder and passes all active QuoteBlockCategory records (ordered by order, with their active, ordered QuoteBlock children) into the view.
2

Block catalogue fetch

The SPA calls GET /api/quote-blocks to retrieve the full block catalogue in JSON. The response shape is:
{
  "success": true,
  "categories": [
    {
      "id": 1,
      "name": "Design",
      "description": "Visual design services",
      "expanded": true,
      "blocks": [
        {
          "id": 12,
          "name": "UI Kit",
          "description": "Complete component library",
          "type": "design",
          "category_id": 1,
          "base_price": 1500.00,
          "default_hours": 40,
          "config": {},
          "order": 1
        }
      ]
    }
  ]
}
Only blocks where is_active = true are included, ordered by the order column.
3

Block selection

The client selects blocks from categorised lists. As each block is toggled, the SPA recalculates:
  • Total price — sum of base_price across selected blocks
  • Total hours — sum of default_hours across selected blocks
  • Subtotal, tax (16 %), and grand total
All calculations happen in the browser; no round-trips are needed for price updates.
4

Client information form

Before submitting, the client fills in their details. The form collects the following fields:
FieldRequiredDescription
client.nameYesFull name
client.emailYesEmail address
client.companyNoCompany or organisation
client.phoneNoPhone number
client.additional_requirementsNoFree-text notes
5

Save draft or submit

The client has two actions available:
  • Save draft — calls POST /api/quotes/save-draft. The quote is stored with status = draft. No PDF is generated and no email is sent.
  • Submit — calls POST /api/quotes/submit. The quote is stored with status = sent, a PDF is generated and stored, and the sent_at timestamp is recorded.
Both actions return the generated reference on success.

API endpoints

Returns all active categories and their active, ordered blocks. No authentication required.Response
{
  "success": true,
  "categories": [ /* ... */ ]
}
Saves the current builder state as a draft. The quote is created with status = draft.Required fields
FieldTypeValidation
client.namestringrequired, max 255
client.emailstringrequired, valid email
blocksarrayrequired
summary.totalnumericrequired, min 0
Success response
{
  "success": true,
  "reference": "COT-63F9A1B2C3D4E",
  "message": "Cotización guardada como borrador"
}
Validation error response (422)
{
  "success": false,
  "errors": { "client.email": ["The client.email field must be a valid email address."] }
}
Submits the quote. The quote is stored with status = sent, a PDF is generated and written to storage/app/public/quotes/{reference}.pdf, and sent_at is set to the current timestamp.Required fields
FieldTypeValidation
client.namestringrequired, max 255
client.emailstringrequired, valid email
blocksarrayrequired, min 1 item
summary.totalnumericrequired
Success response
{
  "success": true,
  "reference": "COT-63F9A1B2C3D4E",
  "pdf_url": "/storage/quotes/COT-63F9A1B2C3D4E.pdf",
  "message": "Cotización enviada exitosamente"
}
Server error response (500)
{
  "success": false,
  "message": "Error al procesar la cotización. Por favor, intenta nuevamente."
}

Request payload structure

Both save-draft and submit accept the same top-level payload:
{
  "client": {
    "name": "Ana García",
    "email": "[email protected]",
    "company": "Startup S.A.",
    "phone": "+52 55 1234 5678",
    "additional_requirements": "Necesito entrega en 3 semanas."
  },
  "blocks": [
    {
      "id": 12,
      "name": "UI Kit",
      "description": "Complete component library",
      "type": "design",
      "base_price": 1500.00,
      "hours": 40,
      "total_price": 1500.00
    }
  ],
  "summary": {
    "subtotal": 1500.00,
    "tax": 240.00,
    "total": 1740.00,
    "hours": 40
  }
}

Quote items

After the Quote record is created, each entry in the blocks array is stored as a QuoteItem. The fields persisted are:
ColumnSource field
quote_block_idblocks[].id
nameblocks[].name
descriptionblocks[].description
typeblocks[].type
quantityblocks[].quantity (default 1)
hoursblocks[].hours
unit_priceblocks[].base_price or blocks[].unit_price
total_priceblocks[].total_price or blocks[].totalPrice
datablocks[].data or blocks[].config

Quote reference format

References are generated automatically in the Quote model’s boot() method when a record is created:
$quote->reference = 'COT-' . strtoupper(uniqid());
This produces references such as COT-63F9A1B2C3D4E. References are unique per server microsecond and require no database sequence.
The draft save and final submit flows are identical in terms of the data they accept. The only differences are that submit enforces at least one block (min:1) and sets status = sent with a sent_at timestamp and a generated PDF.

Build docs developers (and LLMs) love