Documentation
¶
Overview ¶
Package crm is your sales pipeline: the companies, the people, the deals in play.
Plus the Startup Program intake, which lands as a scored application.
The three core entities are faithful to Twenty's `company` / `person` / `opportunity` standard objects, with Twenty's composite fields (FULL_NAME, EMAILS, CURRENCY, LINKS, ADDRESS) flattened to scalar columns for SQLite.
A CRM contact is a PROSPECT the org tracks. It is NOT a product user: an org's own users live in Hanzo IAM and apps/marketing resolves audiences from that roster (roster.go), never from this table. The two contact universes are deliberate and do not join.
Tenant isolation is enforced SERVER-SIDE on every request: the org is the value SanitizeIdentity minted from the VALIDATED bearer owner claim (HIP-0026) — never a client-supplied header, and never a field of a request body. Every store query filters WHERE org=?, so one tenant can never read or mutate another's data.
Surface (all org-scoped; /v1 only):
GET /v1/crm/summary per-org row counts (companies/contacts/opps)
GET /v1/crm/companies list companies -> {data:[…]}
POST /v1/crm/companies create a company -> Company (201)
GET /v1/crm/companies/:id company detail -> Company
PUT /v1/crm/companies/:id update a company -> Company
DELETE /v1/crm/companies/:id delete a company (+ clear refs)
GET /v1/crm/contacts list contacts (?companyId=) -> {data:[…]}
POST /v1/crm/contacts create a contact -> Contact (201)
GET /v1/crm/contacts/:id contact detail -> Contact
PUT /v1/crm/contacts/:id update a contact -> Contact
DELETE /v1/crm/contacts/:id delete a contact (+ clear refs)
GET /v1/crm/opportunities list opportunities (?stage=) -> {data:[…]}
POST /v1/crm/opportunities create an opportunity -> Opportunity (201)
GET /v1/crm/opportunities/:id opportunity detail -> Opportunity
PUT /v1/crm/opportunities/:id update an opportunity -> Opportunity
DELETE /v1/crm/opportunities/:id delete an opportunity
Every route above is a TYPED op, so each is one registry entry with N projections — the REST route, the OpenAPI operation, the /mcp tool, the CLI command and the generated SDK method all follow from it. The Startup Program intake (POST /v1/crm/applications, applications.go) is the ONE route here that stays a raw handler, and says why at its registration.
Order 131: binds /v1/crm/* before the AI subsystem's /v1/* catch-all (150). serve.go auto-registers GET /v1/crm/health.
Index ¶
- Constants
- func Mount(app cloud.Router, deps cloud.Deps) error
- func Shutdown() error
- type Company
- type Contact
- type Opportunity
- type ProgramApplication
- type ScreenResult
- type StageEvent
- type Store
- func (s *Store) Close() error
- func (s *Store) CountApplications(ctx context.Context, org string) (int, error)
- func (s *Store) Counts(ctx context.Context, org string) (companies, contacts, opps int, err error)
- func (s *Store) CreateApplication(ctx context.Context, a ProgramApplication) (ProgramApplication, error)
- func (s *Store) CreateCompany(ctx context.Context, c Company) (Company, error)
- func (s *Store) CreateContact(ctx context.Context, c Contact) (Contact, error)
- func (s *Store) CreateOpportunity(ctx context.Context, o Opportunity) (Opportunity, error)
- func (s *Store) DeleteCompany(ctx context.Context, org, id string) (bool, error)
- func (s *Store) DeleteContact(ctx context.Context, org, id string) (bool, error)
- func (s *Store) DeleteOpportunity(ctx context.Context, org, id string) (bool, error)
- func (s *Store) FindApplicationByEmailCompany(ctx context.Context, org, email, company string) (ProgramApplication, error)
- func (s *Store) GetApplication(ctx context.Context, org, id string) (ProgramApplication, error)
- func (s *Store) GetCompany(ctx context.Context, org, id string) (Company, error)
- func (s *Store) GetContact(ctx context.Context, org, id string) (Contact, error)
- func (s *Store) GetOpportunity(ctx context.Context, org, id string) (Opportunity, error)
- func (s *Store) ListApplications(ctx context.Context, org, stage string, limit int) ([]ProgramApplication, error)
- func (s *Store) ListCompanies(ctx context.Context, org string, limit int) ([]Company, error)
- func (s *Store) ListContacts(ctx context.Context, org, companyID string, limit int) ([]Contact, error)
- func (s *Store) ListOpportunities(ctx context.Context, org, stage string, limit int) ([]Opportunity, error)
- func (s *Store) UpdateApplication(ctx context.Context, a ProgramApplication) (ProgramApplication, error)
- func (s *Store) UpdateCompany(ctx context.Context, c Company) (Company, error)
- func (s *Store) UpdateContact(ctx context.Context, c Contact) (Contact, error)
- func (s *Store) UpdateOpportunity(ctx context.Context, o Opportunity) (Opportunity, error)
Constants ¶
const ( StageApplied = "applied" StageScreened = "screened" StageQualified = "qualified" StageCreditsOffered = "credits-offered" StageOnboarded = "onboarded" StageRejected = "rejected" )
Startup pipeline stages, in order. `rejected` is an off-pipeline terminal.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Company ¶
type Company struct {
// ID is the server-minted company id ("comp_" + 128 random bits).
ID string `json:"id"`
// Org is the owning tenant. Never on the wire: it is the isolation key the
// server reads from the validated principal, not a field a caller sends or reads.
Org string `json:"-"`
// Name is the company name.
Name string `json:"name"`
// DomainName is the company's primary domain, e.g. "acme.com".
DomainName string `json:"domainName"`
// Employees is the headcount.
Employees int64 `json:"employees"`
// City is the head-office city.
City string `json:"city"`
// Country is the head-office country.
Country string `json:"country"`
// ARR is annual recurring revenue in minor units (cents) of Currency.
ARR int64 `json:"arr"`
// Currency is the ISO code ARR is denominated in; a write that names none
// stores USD.
Currency string `json:"currency"`
// ICP marks the company as an ideal-customer-profile fit.
ICP bool `json:"idealCustomerProfile"`
// Linkedin is the company's LinkedIn URL.
Linkedin string `json:"linkedinLink"`
// XLink is the company's X (Twitter) URL.
XLink string `json:"xLink"`
// CreatedAt is the unix second the company was created. Server-owned.
CreatedAt int64 `json:"createdAt"`
// UpdatedAt is the unix second of the last write. Server-owned.
UpdatedAt int64 `json:"updatedAt"`
}
Company is an org-scoped account record, faithful to Twenty's `company` standard object (composites flattened to scalar columns for SQLite): ARR is minor units (cents) of Currency; ICP is the ideal-customer-profile flag.
type Contact ¶
type Contact struct {
// ID is the server-minted contact id ("cont_" + 128 random bits).
ID string `json:"id"`
// Org is the owning tenant. Never on the wire: it is the isolation key the
// server reads from the validated principal, not a field a caller sends or reads.
Org string `json:"-"`
// FirstName is the person's given name.
FirstName string `json:"firstName"`
// LastName is the person's family name.
LastName string `json:"lastName"`
// Email is the person's email address.
Email string `json:"email"`
// Phone is the person's phone number.
Phone string `json:"phone"`
// JobTitle is the person's role at their company.
JobTitle string `json:"jobTitle"`
// City is where the person is based.
City string `json:"city"`
// CompanyID links the contact to one of the org's companies; empty when the
// contact stands alone, and cleared when its company is deleted. A write
// naming a company the org does not own is refused with 422.
CompanyID string `json:"companyId"`
// Linkedin is the person's LinkedIn URL.
Linkedin string `json:"linkedinLink"`
// XLink is the person's X (Twitter) URL.
XLink string `json:"xLink"`
// CreatedAt is the unix second the contact was created. Server-owned.
CreatedAt int64 `json:"createdAt"`
// UpdatedAt is the unix second of the last write. Server-owned.
UpdatedAt int64 `json:"updatedAt"`
}
Contact is an org-scoped person record, faithful to Twenty's `person` standard object (FULL_NAME/EMAILS/PHONES composites flattened). CompanyID is an optional in-org relation to a Company.
type Opportunity ¶
type Opportunity struct {
// ID is the server-minted opportunity id ("oppo_" + 128 random bits).
ID string `json:"id"`
// Org is the owning tenant. Never on the wire: it is the isolation key the
// server reads from the validated principal, not a field a caller sends or reads.
Org string `json:"-"`
// Name is the deal name.
Name string `json:"name"`
// Amount is the deal value in minor units (cents) of Currency.
Amount int64 `json:"amount"`
// Currency is the ISO code Amount is denominated in; a write that names none
// stores USD.
Currency string `json:"currency"`
// Stage is the pipeline stage, always one of NEW, SCREENING, MEETING,
// PROPOSAL or CUSTOMER — stored upper-case whatever case the write used.
Stage string `json:"stage"`
// CloseDate is the expected close as a unix second (0 = unset).
CloseDate int64 `json:"closeDate"`
// CompanyID links the deal to one of the org's companies; empty when
// unlinked, and cleared when that company is deleted. A write naming a
// company the org does not own is refused with 422.
CompanyID string `json:"companyId"`
// PointOfContact links the deal to one of the org's contacts; empty when
// unlinked, and cleared when that contact is deleted. A write naming a
// contact the org does not own is refused with 422.
PointOfContact string `json:"pointOfContactId"`
// CreatedAt is the unix second the opportunity was created. Server-owned.
CreatedAt int64 `json:"createdAt"`
// UpdatedAt is the unix second of the last write. Server-owned.
UpdatedAt int64 `json:"updatedAt"`
}
Opportunity is an org-scoped deal record, faithful to Twenty's `opportunity` standard object. Amount is minor units (cents) of Currency; CloseDate is a unix second (0 == unset). Stage is validated against the default pipeline.
type ProgramApplication ¶ added in v1.801.490
type ProgramApplication struct {
// ID is the server-minted application id ("appl_" + 128 random bits).
ID string `json:"id"`
// Org is the owning tenant — the program org, which is the deployment brand.
// Never on the wire: it is the isolation key the server reads, not a field a
// caller sends or reads.
Org string `json:"-"`
// Company is the applicant's company name.
Company string `json:"company"`
// Website is the applicant's website as submitted.
Website string `json:"website"`
// ContactName is the person who applied.
ContactName string `json:"contactName"`
// Email is the applicant's email — half of the (email, company) key a
// resubmission refreshes instead of duplicating.
Email string `json:"email"`
// Role is the applicant's role at their company.
Role string `json:"role"`
// Stage is the pipeline stage: applied, screened, qualified, credits-offered,
// onboarded or rejected. Server-owned — it starts at "applied" and moves only
// through the transition machine.
Stage string `json:"stage"`
// Tier1 is whether the applicant is tier-1 backed, derived deterministically
// at intake from the submitted fund list — independent of the AI screen.
Tier1 bool `json:"tier1"`
// Metadata is the FULL submitted form, every field, including the arrays the
// promoted columns above do not carry (tier1Investors, useCases) and the
// deterministic tier1Matched list.
Metadata map[string]any `json:"metadata"`
// Screen is the AI screen. It runs after intake, so a freshly created
// application carries a "pending" screen.
Screen ScreenResult `json:"screen"`
// Events is the append-only stage-transition log, oldest first.
Events []StageEvent `json:"events"`
// CompanyID is the CRM Company minted for this lead at intake, so the startup
// also appears in the org's standard CRM tabs. Empty when that best-effort
// projection did not run.
CompanyID string `json:"companyId"`
// ContactID is the CRM Contact minted for this lead at intake. Empty when that
// best-effort projection did not run.
ContactID string `json:"contactId"`
// Reason is why the application was rejected, required to reject. Empty
// otherwise.
Reason string `json:"reason"`
// CreatedAt is the unix second the application arrived. Server-owned.
CreatedAt int64 `json:"createdAt"`
// UpdatedAt is the unix second of the last write. Server-owned.
UpdatedAt int64 `json:"updatedAt"`
}
ProgramApplication is one startup-program submission plus its AI screen and pipeline state. Metadata carries the FULL submitted payload (all form fields, including arrays like tier1Investors/useCases); the promoted columns are query/display projections. Tier1 is deterministically derived at intake from the submitted fund list (independent of the AI screen's judgement).
type ScreenResult ¶
type ScreenResult struct {
// Status is the screen's state: pending | done | failed.
Status string `json:"status"`
// Score is the model's 0..100 fit score, clamped to that range.
Score int `json:"score"`
// Tier1Backed is the model's read on tier-1 backing, normalized to
// "yes", "no" or "unclear" (anything it cannot resolve reads "unclear").
Tier1Backed string `json:"tier1Backed"`
// SuggestedCredits is the recommended credit grant in USD, snapped to the
// nearest allowed rung: 0 | 5000 | 25000 | 50000 | 150000.
SuggestedCredits int `json:"suggestedCredits"`
// Summary is the model's short assessment of the application.
Summary string `json:"summary"`
// DraftReply is a suggested email reply for staff to edit and send.
DraftReply string `json:"draftReply"`
// Model is the LLM the screen ran on.
Model string `json:"model"`
// ScreenedAt is the unix second the screen finished (0 while pending).
ScreenedAt int64 `json:"screenedAt"`
// Error says why a failed screen failed — no AI gateway configured, a gateway
// error, or a reply that carried no parseable JSON. Absent on success.
Error string `json:"error,omitempty"`
}
ScreenResult is the AI screen stored on an application. Status is pending → done | failed; a failed/absent screen never blocks intake.
type StageEvent ¶
type StageEvent struct {
// From is the stage moved out of; empty on the intake event that opens the log.
From string `json:"from"`
// To is the stage moved into.
To string `json:"to"`
// At is the unix second of the move.
At int64 `json:"at"`
// By is who moved it: "system" for intake and the AI auto-advance, else the
// validated staff user id.
By string `json:"by"`
// Note is the free-text comment recorded with the move. Absent when none.
Note string `json:"note,omitempty"`
}
StageEvent is one entry in an application's append-only stage-transition log.
type Store ¶
type Store struct {
// contains filtered or unexported fields
}
Store is the CRM database. ONE SQLite file — the system namespace's "crm" — holds every org's records; tenant isolation is the `org` column, enforced on EVERY query. This mirrors clients/prompts and clients/eval exactly (the ONE storage pattern). MaxOpenConns(1) serializes writes against the single-writer file.
func (*Store) CountApplications ¶
CountApplications returns the org's application count (overview cards).
func (*Store) Counts ¶
Counts returns per-org row counts across the three entities — a real, non-fabricated summary for the CRM module's overview cards.
func (*Store) CreateApplication ¶
func (s *Store) CreateApplication(ctx context.Context, a ProgramApplication) (ProgramApplication, error)
func (*Store) CreateCompany ¶
func (*Store) CreateContact ¶
func (*Store) CreateOpportunity ¶
func (s *Store) CreateOpportunity(ctx context.Context, o Opportunity) (Opportunity, error)
func (*Store) DeleteCompany ¶
DeleteCompany removes a company and NULLs any dangling contact/opportunity refs to it within the same org (no orphaned foreign refs). One transaction.
func (*Store) DeleteContact ¶
DeleteContact removes a contact and clears any opportunity point-of-contact refs to it within the org. One transaction.
func (*Store) DeleteOpportunity ¶
func (*Store) FindApplicationByEmailCompany ¶
func (s *Store) FindApplicationByEmailCompany(ctx context.Context, org, email, company string) (ProgramApplication, error)
FindApplicationByEmailCompany returns the org's application matching a case-insensitive (email, company) pair, or errNotFound. Basis for idempotent intake (a resubmission updates rather than duplicates).
func (*Store) GetApplication ¶
func (*Store) GetCompany ¶
func (*Store) GetContact ¶
func (*Store) GetOpportunity ¶
func (*Store) ListApplications ¶
func (s *Store) ListApplications(ctx context.Context, org, stage string, limit int) ([]ProgramApplication, error)
ListApplications lists an org's applications, optionally filtered by pipeline stage (stage=="" means all). Newest first.
func (*Store) ListCompanies ¶
func (*Store) ListContacts ¶
func (s *Store) ListContacts(ctx context.Context, org, companyID string, limit int) ([]Contact, error)
ListContacts lists the org's contacts, optionally filtered to one company (companyID=="" means all). Most-recently-updated first.
func (*Store) ListOpportunities ¶
func (s *Store) ListOpportunities(ctx context.Context, org, stage string, limit int) ([]Opportunity, error)
ListOpportunities lists the org's opportunities, optionally filtered by stage (stage=="" means all). Most-recently-updated first.
func (*Store) UpdateApplication ¶
func (s *Store) UpdateApplication(ctx context.Context, a ProgramApplication) (ProgramApplication, error)
UpdateApplication persists the mutable columns (stage, tier1, metadata, screen, events, links, reason). ID/Org/CreatedAt are immutable keys.
func (*Store) UpdateCompany ¶
func (*Store) UpdateContact ¶
func (*Store) UpdateOpportunity ¶
func (s *Store) UpdateOpportunity(ctx context.Context, o Opportunity) (Opportunity, error)