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:
-
Update quote:
status = accepted
accepted_option_id = chosen option
accepted_at = now
-
Create Order:
source = "quote"
- Populate customer data from Customers table
-
Create OrderItems:
- Only from
printing type line items in accepted option
-
Set:
quote.order_id = new order ID
-
Record events on:
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
-
Repository tests
go test -v ./internal/repository/...
-
Service tests
go test -v ./internal/service/...
-
Full backend suite
-
Frontend type check
cd web && npx tsc --noEmit
-
Manual verification flow
- Create customer
- Create quote with 2 options
- Add line items
- Send quote
- Accept Option A
- Confirm Order created with correct data
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 / ExpiredWhen a quote is accepted, it converts into an Order and flows into the existing fulfillment pipeline.
This introduces three new entities:
This is additive:
Data Model
Customers
Orders table modification:
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 )Statuses
Line Item Types
Each line item includes:
No project/material linkage required.
Quote → Order Conversion
When a quote is accepted with a selected option:
Update quote:
status = acceptedaccepted_option_id = chosen optionaccepted_at = nowCreate Order:
source = "quote"Create OrderItems:
printingtype line items in accepted optionSet:
quote.order_id = new order IDRecord events on:
API Endpoints
Customers
Quotes
Quote Options
Quote Line Items
Implementation Plan
Phase 1: Schema + Migration
internal/database/schema.sqlmigrations/016_customers_and_quotes.sqlPhase 2: Go Model Types
internal/model/models.goCustomerID *uuid.UUIDto Order. AddOrderSourceQuoteconstant.Phase 3: Repository Layer
internal/repository/customer.gointernal/repository/quote.gointernal/repository/repository.gointernal/repository/order.goPhase 4: Service Layer
internal/service/customer.gointernal/service/quote.gointernal/service/service.goPhase 5: API Handlers
internal/api/customer_handler.gointernal/api/quote_handler.gointernal/api/router.go/api/customersand/api/quotesroute groupsPhase 6: Frontend
web/src/types/index.tsweb/src/api/client.tsweb/src/pages/Quotes.tsxweb/src/pages/QuoteDetail.tsxweb/src/pages/Customers.tsxweb/src/pages/CustomerDetail.tsxweb/src/App.tsx/quotes,/quotes/:id,/customers,/customers/:idweb/src/components/Layout.tsxPhase 7: Tests
internal/repository/customer_test.gointernal/repository/quote_test.gointernal/service/quote_test.goVerification Checklist
Repository tests
go test -v ./internal/repository/...Service tests
go test -v ./internal/service/...Full backend suite
make testFrontend type check
Manual verification flow