Skip to content

Custom Quote Workflows #23

@philjestin

Description

@philjestin

Problem

from reddit

Only other thing I could think of would be something to support more on the custom quote side. To provide a little background, I almost exclusively do custom jobs, not prints from a catalog, and as a result am often making 3-4 quotes for each customer at a time. Each quote can be different offerings of material and scope that they can choose to move forward with. My current system is a highly customized sheet that allows me to update the status of each quote (Draft, Sent, Accepted, Rejected). I think this would likely require an additional quote building section for this as well as a job status page, because my quotes are based on machine time and material (for printing) as well as post-processing requirements, consulting and design requirements and other unique job descriptions. That way each individual quote’s status can be updated as I mentioned before.

Custom Quote System (Additive Consulting)

Overview

A user running an additive consulting company needs custom quoting instead of catalog-based orders.

They:

  • Build 3–4 quotes per customer

  • Include multiple options per quote (different materials/scope)

  • Require status tracking:

    Draft → Sent → Accepted → Rejected / Expired

When a quote is accepted, it converts into an Order and flows into the existing fulfillment pipeline.

This introduces three new entities:

  • Customers
  • Quotes (with Options and Line Items)
  • Quote → Order conversion flow

This is additive:

  • Catalog-based users never touch quotes.
  • Custom users skip the catalog entirely and operate within quotes.

Data Model

Customers

customers (
  id,
  name,
  email,
  company,
  phone,
  notes,
  created_at,
  updated_at
)

Orders table modification:

ALTER TABLE orders ADD COLUMN customer_id UUID NULL;

Quotes

quotes (
  id,
  quote_number,          -- Q-0001
  customer_id,
  status,
  title,
  notes,
  valid_until,
  accepted_option_id,
  order_id,
  created_at,
  updated_at,
  sent_at,
  accepted_at
)
quote_options (
  id,
  quote_id,
  name,
  description,
  sort_order,
  total_cents,
  created_at
)
quote_line_items (
  id,
  option_id,
  type,
  description,
  quantity,              -- REAL (fractional hours supported)
  unit,                  -- hours / units / grams / each
  unit_price_cents,
  total_cents,
  sort_order,
  created_at
)
quote_events (
  id,
  quote_id,
  event_type,
  message,
  created_at
)

Statuses

draft → sent → accepted / rejected / expired

Line Item Types

printing
post_processing
consulting
design
other

Each line item includes:

  • description
  • quantity
  • unit
  • rate
  • total

No project/material linkage required.


Quote → Order Conversion

When a quote is accepted with a selected option:

  1. Update quote:

    • status = accepted
    • accepted_option_id = chosen option
    • accepted_at = now
  2. Create Order:

    • source = "quote"
    • Populate customer data from Customers table
  3. Create OrderItems:

    • Only from printing type line items in accepted option
  4. Set:

    • quote.order_id = new order ID
  5. Record events on:

    • Quote
    • Order

API Endpoints

Customers

GET    /api/customers              -- list (search param)
POST   /api/customers              -- create

GET    /api/customers/{id}         -- get
PATCH  /api/customers/{id}         -- update
DELETE /api/customers/{id}         -- delete

Quotes

GET    /api/quotes                 -- list (status, customer_id filters)
POST   /api/quotes                 -- create

GET    /api/quotes/{id}
PATCH  /api/quotes/{id}
DELETE /api/quotes/{id}

POST   /api/quotes/{id}/send       -- draft → sent
POST   /api/quotes/{id}/accept     -- body: { option_id }
POST   /api/quotes/{id}/reject     -- sent → rejected

Quote Options

POST   /api/quotes/{id}/options
PATCH  /api/quotes/{id}/options/{optionId}
DELETE /api/quotes/{id}/options/{optionId}

Quote Line Items

POST   /api/quotes/{id}/options/{optionId}/items
PATCH  /api/quotes/{id}/options/{optionId}/items/{itemId}
DELETE /api/quotes/{id}/options/{optionId}/items/{itemId}

Implementation Plan

Phase 1: Schema + Migration

File Action
internal/database/schema.sql Add customers, quotes, quote_options, quote_line_items, quote_events
migrations/016_customers_and_quotes.sql Same tables + ALTER TABLE orders ADD COLUMN customer_id

Phase 2: Go Model Types

File Action
internal/model/models.go Add Customer, Quote, QuoteOption, QuoteLineItem, QuoteEvent, QuoteStatus, QuoteLineItemType, QuoteFilters, CustomerFilters. Add CustomerID *uuid.UUID to Order. Add OrderSourceQuote constant.

Phase 3: Repository Layer

File Action
internal/repository/customer.go New. CRUD + LIKE search (name/email/company)
internal/repository/quote.go New. Quote CRUD + NextQuoteNumber. Option CRUD. LineItem CRUD. RecalculateOptionTotal (SUM line items). Event add/list.
internal/repository/repository.go Add Customers and Quotes to struct + NewRepositories()
internal/repository/order.go Add customer_id to INSERT/SELECT/UPDATE

Phase 4: Service Layer

File Action
internal/service/customer.go New. CRUD + validation (name required) + WebSocket broadcasts
internal/service/quote.go New. CRUD + Send/Accept/Reject transitions. Option CRUD. LineItem CRUD with auto-recalculate totals. Accept() creates Order from selected option.
internal/service/service.go Add Customers and Quotes to Services struct. Initialize in NewServices().

Phase 5: API Handlers

File Action
internal/api/customer_handler.go New. List, Create, Get, Update, Delete
internal/api/quote_handler.go New. Full CRUD + Send/Accept/Reject + Option + LineItem handlers (~14 methods)
internal/api/router.go Add /api/customers and /api/quotes route groups

Phase 6: Frontend

File Action
web/src/types/index.ts Add Customer, Quote, QuoteOption, QuoteLineItem, QuoteEvent, QuoteStatus, QuoteLineItemType
web/src/api/client.ts Add customersApi and quotesApi
web/src/pages/Quotes.tsx New list page with status filter
web/src/pages/QuoteDetail.tsx New detail page with options cards, line item tables, Send/Accept/Reject buttons, events timeline
web/src/pages/Customers.tsx New searchable list
web/src/pages/CustomerDetail.tsx Customer info + quote/order history tabs
web/src/App.tsx Add routes: /quotes, /quotes/:id, /customers, /customers/:id
web/src/components/Layout.tsx Add Quotes and Customers nav links

Phase 7: Tests

File Action
internal/repository/customer_test.go CRUD + search filtering
internal/repository/quote_test.go Quote CRUD, NextQuoteNumber, Option CRUD, LineItem CRUD, total recalculation, events, cascade delete
internal/service/quote_test.go Validation, status transitions, Accept creates Order, line item total recalculation

Verification Checklist

  1. Repository tests

    go test -v ./internal/repository/...
  2. Service tests

    go test -v ./internal/service/...
  3. Full backend suite

    make test
  4. Frontend type check

    cd web && npx tsc --noEmit
  5. Manual verification flow

    • Create customer
    • Create quote with 2 options
    • Add line items
    • Send quote
    • Accept Option A
    • Confirm Order created with correct data

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions