Get Hedhog updates in your inbox
New releases, fresh recipes, and breaking changes — no spam.
CRM Submodule
proposals
Commercial proposal and contract lifecycle — draft, submit for approval, approve/reject/cancel, convert to contract, and generate PDF documents.
Source path: libraries/crm/src/proposal
Module file: proposal.module.ts
Introduction
The Proposals submodule models the full lifecycle of a commercial offer: draft creation, internal approval, client delivery, and conversion into a contract. A proposal moves through a fixed status machine — draft, pending_approval, approved, rejected, cancelled, expired, contract_generated — and every state-changing action is exposed as its own endpoint (submit, send, approve, reject, cancel, convert) rather than a generic status PATCH, which keeps the audit trail and side effects (notifications, approval records) attached to an explicit business action instead of an arbitrary field write.
Internally, every proposal keeps a revision history. PATCH /proposal/:id normally updates the current revision in place, but passing create_new_revision: true forks a new revision (incrementing current_revision_number) instead of overwriting — this lets a sales rep iterate on pricing or scope after a proposal has already been sent without losing the version the client originally saw. Line items (services, products, fees, discounts, notes) and attached documents are children of a revision, not of the proposal itself, so each revision carries its own complete snapshot.
Approval is configurable rather than hardcoded: the number of approvals required before a proposal is considered fully approved is read from the crm-proposal-required-approvals setting (default 1), and each approval is recorded as a row with its own approver_user_id and status so multiple approvers can weigh in independently. Critically, POST /proposal/:id/approve is gated by a dedicated proposal-approver role rather than the general admin-crm role used by the rest of the submodule — this lets an organization designate specific people (e.g. a finance lead or department head) as approval authorities without granting them full CRM administration rights. Submit and send currently route through the same submitForApproval flow on the service, so "sending" a proposal to a client and "submitting" it for internal review are functionally identical today.
POST /proposal/:id/generate-pdf renders the current revision to HTML and then to a PDF buffer, uploads the resulting file through the platform File service (under contact/proposals/generated), and links it back to the proposal as a generated_pdf document — meaning every PDF a proposal ever produced remains retrievable through its document history, not just the latest one. Proposal documents can also originate from a manual upload, an AI-assisted extraction, or an import, distinguished by source_kind, and carry their own extraction_status for tracking OCR/parsing of uploaded source files.
POST /proposal/:id/convert requests conversion of an approved proposal into a contract; this raises an integration event that downstream listeners (such as the checkout/contract listeners wired in CrmModule) can react to, keeping the proposal-to-contract handoff decoupled from the Proposals controller itself.
HTTP Endpoints
13 endpoints
Paginated list of proposals with optional status and person filters. Each item includes required_approvals, approval_count, and whether the current user has already approved it.
- Query
- page, pageSize, search, status? (draft | pending_approval | approved | rejected | cancelled | expired | contract_generated), person_id?
Summary statistics for proposals (counts per status, total values).
Get full proposal details including items, documents, and approval/workflow history for the current revision.
- Params
- id (int)
Create a new proposal (defaults to draft status).
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| person_id | int | yes | — |
| title | string (max 180) | yes | — |
| code | string (max 40) | no | — |
| status | ProposalStatus (draft | pending_approval | ...) | no | — |
| contract_category | ProposalContractCategory (employee | contractor | client | supplier | vendor | partner | internal | other) | no | — |
| contract_type | ProposalContractType (clt | pj | freelancer_agreement | service_agreement | fixed_term | recurring_service | nda | amendment | addendum | other) | no | — |
| billing_model | ProposalBillingModel (time_and_material | monthly_retainer | fixed_price) | no | — |
| currency_code | string | no | — |
| valid_from | date string | no | — |
| valid_until | date string | no | — |
| subtotal_amount_cents | int | no | — |
| discount_amount_cents | int | no | — |
| tax_amount_cents | int | no | — |
| total_amount_cents | int | no | — |
| notes | string | no | — |
| owner_user_id | int | no | — |
| generation_mode | ProposalGenerationMode (manual | ai_assisted | duplicated | imported) | no | — |
| summary | string | no | — |
| content_html | string | no | — |
| snapshot_json | object | no | — |
| items | ProposalItemDto[] | no | Line items (services, products, fees, discounts) |
| documents | ProposalDocumentDto[] | no | Attached documents |
Update proposal fields (partial update; all CreateProposalDto fields accepted).
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| create_new_revision | boolean | no | Fork a new revision instead of overwriting the current one |
Render the current revision to PDF, upload it via the File service, and attach it to the proposal as a generated_pdf document.
- Params
- id (int)
Submit the proposal for internal approval.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| approver_user_id | int | no | — |
| note | string | no | — |
Send the proposal to the client (routes through the same submitForApproval flow as /submit).
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| approver_user_id | int | no | — |
| note | string | no | — |
Approve a proposal that is pending approval. Restricted to the dedicated proposal-approver role, not the general admin-crm role.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| note | string | no | — |
Reject a proposal that is pending approval.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| note | string | no | — |
Cancel a proposal.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| note | string | no | — |
Request conversion of an approved proposal to a contract; raises an integration event for downstream listeners.
- Params
- id (int)
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| note | string | no | — |
Delete one or more proposals.
Body
| Field | Type | Required | Notes |
|---|---|---|---|
| ids | number[] | yes | IDs to delete |
MCP Tools
Named tools callable by AI models via the Model Context Protocol (MCP).
crm.proposals.listLists proposals with pagination and optional filters
crm.proposals.statsReturns proposal statistics grouped by status
crm.proposals.getReturns a single proposal by ID with full details including items and documents
crm.proposals.createCreates a new proposal for a person
crm.proposals.updateUpdates an existing proposal by ID
crm.proposals.submitSubmits a proposal for approval
crm.proposals.sendSends a proposal to the client
crm.proposals.approveApproves a proposal. Requires proposal-approver role
crm.proposals.rejectRejects a proposal with an optional reason
crm.proposals.cancelCancels a proposal
crm.proposals.convertRequests conversion of an approved proposal into a contract
crm.proposals.deleteDeletes one or more proposals by their IDs