// Package recruiting is the recruiting operations module: the clients an organization recruits for, the // people at those clients, and later the job orders placed against them. It is the write side that // recruitMATCH reports on. // // Two decisions shape every write in this package. // // Concurrency is optimistic, not last write wins. Every update carries the updated_at the caller read, // and the UPDATE matches on it. Two recruiters editing the same client is uncommon but silently losing // one of their edits is unacceptable, and a timestamptz round trips through RFC3339Nano exactly, so the // token needs no schema change and no version column. // // Constraint violations are translated, never leaked. A duplicate client name is a 409 that names the // problem, not a 500 with a Postgres string in it. The database is the authority on uniqueness because // only the database can check it without a race, so the service inserts and interprets the failure // rather than pre checking and hoping nothing changes in between. package recruiting import "time" // Client is a company this organization recruits for. type Client struct { ID string `json:"id"` Name string `json:"name"` UEI string `json:"uei,omitempty"` Industry string `json:"industry,omitempty"` Website string `json:"website,omitempty"` Status string `json:"status"` Notes string `json:"notes,omitempty"` ContactCount int `json:"contact_count"` // Ownership is the lock. A client with an owner is edited by that owner; an unowned client is // claimable by anyone with write capability. OwnerName is carried so the UI can say who holds an // account without a second lookup, because the answer to a collision is a conversation. OwnerUserID string `json:"owner_user_id,omitempty"` OwnerName string `json:"owner_name,omitempty"` OwnerSince string `json:"owner_since,omitempty"` CreatedAt string `json:"created_at"` // UpdatedAt is both a display value and the optimistic concurrency token. A caller sends it back on // an update and the write is refused if the row moved underneath them. UpdatedAt string `json:"updated_at"` } // Contact is a person at a client. type Contact struct { ID string `json:"id"` ClientID string `json:"client_id"` FullName string `json:"full_name"` Title string `json:"title,omitempty"` Email string `json:"email,omitempty"` Phone string `json:"phone,omitempty"` IsHiringManager bool `json:"is_hiring_manager"` IsPrimary bool `json:"is_primary"` Status string `json:"status"` CreatedAt string `json:"created_at"` UpdatedAt string `json:"updated_at"` } // ClientDetail is a client with its people, which is how the detail screen reads it. type ClientDetail struct { Client Contacts []Contact `json:"contacts"` } // ClientList is an envelope rather than a bare array so the caller learns whether more pages exist // without issuing a second count query. type ClientList struct { Clients []Client `json:"clients"` Limit int `json:"limit"` Offset int `json:"offset"` Total int `json:"total"` } // CreateClientInput is the create body. Only Name is required; a client is often entered from a phone // call with nothing else known yet, and forcing more would push people to type placeholders. type CreateClientInput struct { Name string `json:"name"` UEI string `json:"uei"` Industry string `json:"industry"` Website string `json:"website"` Status string `json:"status"` Notes string `json:"notes"` } // UpdateClientInput carries every editable field plus the concurrency token. Fields are pointers so an // omitted field means leave it alone, which is different from an empty string meaning clear it. type UpdateClientInput struct { ExpectedUpdatedAt string `json:"expected_updated_at"` Name *string `json:"name,omitempty"` UEI *string `json:"uei,omitempty"` Industry *string `json:"industry,omitempty"` Website *string `json:"website,omitempty"` Status *string `json:"status,omitempty"` Notes *string `json:"notes,omitempty"` } type CreateContactInput struct { FullName string `json:"full_name"` Title string `json:"title"` Email string `json:"email"` Phone string `json:"phone"` IsHiringManager bool `json:"is_hiring_manager"` IsPrimary bool `json:"is_primary"` } type UpdateContactInput struct { ExpectedUpdatedAt string `json:"expected_updated_at"` FullName *string `json:"full_name,omitempty"` Title *string `json:"title,omitempty"` Email *string `json:"email,omitempty"` Phone *string `json:"phone,omitempty"` IsHiringManager *bool `json:"is_hiring_manager,omitempty"` IsPrimary *bool `json:"is_primary,omitempty"` Status *string `json:"status,omitempty"` } // ts renders a database timestamp for the wire. RFC3339Nano is used rather than RFC3339 because it // preserves the microsecond precision Postgres stores, which is what makes updated_at usable as a // concurrency token: a truncated value would never match on the way back. func ts(t time.Time) string { return t.UTC().Format(time.RFC3339Nano) }