Clinical suggestions: DTO and service-port contract
This is the contract specification for the implementation plan. The Go definitions below document the implemented shared domain and service boundaries. CRUD uses soft deletion and presence-aware PATCH semantics.
1. Shared domain package and dependency rules
Use two packages inside the existing shared Go module:
pkg/types/domain/ package domain
doc.go domain ownership and dependency rules
clinical_models.go catalog models, queries/pages, commands, audit snapshots
clinical_validation.go constants, errors, normalization and validation
pkg/types/ports/ package ports
doc.go service and repository dependency rules
clinical_services.go AdminClinicalSuggestionService and DoctorClinicalService
clinical_repositories.go repositories and mutation transaction interfacesImport paths are github.com/prescriptoo/prescriptoo-api/pkg/types/domain and github.com/prescriptoo/prescriptoo-api/pkg/types/ports. Domain contains the models, inputs, errors and validation. Ports contains the service and repository interfaces, whose signatures use domain values. Domain does not import ports. Both packages exclude Huma/JSON schema tags, HTTP envelopes, bearer-token processing, SQL/pgx types, adapters and imports from services/*.
Do not add feature contracts to services/prescription/public or duplicate the models/interfaces under service-local domain/ports packages. Implementations and private storage records stay inside the owning service. Unrelated existing root pkg/types contracts and their consumers remain outside this clinical refactor.
Doctor and Admin own distinct HTTP request/response DTOs in their presentation packages. Each operation has an explicit mapper from request DTO to domain input and from domain result to response DTO. Document exported fields and mapper behavior; do not serialize a domain model directly or alias an HTTP DTO to it.
Prescription implements the shared service ports. The composition root supplies ports.DoctorClinicalService to Doctor and ports.AdminClinicalSuggestionService to Admin. Repository ports are supplied only to Prescription use cases. The shared location does not make repositories a consumer-facing service API.
2. Route and DTO inventory
Use these names for the operation-specific DTOs. Every response DTO wraps a body containing message, status_code, and data. List data is always a non-null array.
| Method and path | Request DTO | Response DTO | Service port method |
|---|---|---|---|
GET /api/v1/doctors/suggestions/chief-complaints | DoctorChiefComplaintListReqDto | DoctorChiefComplaintListResDto | SuggestChiefComplaints |
GET /api/v1/doctors/suggestions/history | DoctorHistorySuggestionListReqDto | DoctorHistorySuggestionListResDto | SuggestHistory |
GET /api/v1/admin/suggestions/chief-complaints | AdminChiefComplaintListReqDto | AdminChiefComplaintListResDto | ListChiefComplaints |
GET /api/v1/admin/suggestions/chief-complaints/{id} | AdminChiefComplaintGetReqDto | AdminChiefComplaintGetResDto | GetChiefComplaint |
POST /api/v1/admin/suggestions/chief-complaints | AdminChiefComplaintCreateReqDto | AdminChiefComplaintCreateResDto | CreateChiefComplaint |
PATCH /api/v1/admin/suggestions/chief-complaints/{id} | AdminChiefComplaintPatchReqDto | AdminChiefComplaintPatchResDto | PatchChiefComplaint |
DELETE /api/v1/admin/suggestions/chief-complaints/{id} | AdminChiefComplaintDeleteReqDto | AdminChiefComplaintDeleteResDto | DeleteChiefComplaint |
GET /api/v1/admin/suggestions/history | AdminHistorySuggestionListReqDto | AdminHistorySuggestionListResDto | ListHistorySuggestions |
GET /api/v1/admin/suggestions/history/{id} | AdminHistorySuggestionGetReqDto | AdminHistorySuggestionGetResDto | GetHistorySuggestion |
POST /api/v1/admin/suggestions/history | AdminHistorySuggestionCreateReqDto | AdminHistorySuggestionCreateResDto | CreateHistorySuggestion |
PATCH /api/v1/admin/suggestions/history/{id} | AdminHistorySuggestionPatchReqDto | AdminHistorySuggestionPatchResDto | PatchHistorySuggestion |
DELETE /api/v1/admin/suggestions/history/{id} | AdminHistorySuggestionDeleteReqDto | AdminHistorySuggestionDeleteResDto | DeleteHistorySuggestion |
All requests contain a hidden Authorization header DTO field. An active doctor session is required for Doctor routes. Admin list/get require an active platform-admin session and admin.clinical_suggestion.read; create/patch/delete require admin.clinical_suggestion.manage. Permissions are checked on every request, before invoking the provider.
The two Doctor lists are unpaginated autocomplete results. Admin lists return a top-level pagination: {"next_cursor": "..."}; an empty cursor means the final page. Detail/create/patch return one resource. Delete returns data: {} and HTTP/status code 200. Create returns 201; other successful operations return 200.
3. Request DTO fields and validation
Doctor list requests
| DTO field | HTTP source | Validation and meaning |
|---|---|---|
Authorization | Header | Hidden from examples/schema properties; active doctor session required |
Search | search query | Optional, at most 255 Unicode characters; trim/normalize; empty returns defaults |
Limit | limit query | Optional, default 20; integer 1–100 |
Category | category query, history only | Required; one of the 12 categories in the implementation plan |
Chief complaint requests reject category. Both reject unknown parameters. History category selection is strict. These requests do not accept organization, patient, status, deletion, or cursor parameters.
Admin list/get/delete requests
Admin list DTOs accept Search and Limit with the same limits, plus:
| DTO field | HTTP source | Validation and meaning |
|---|---|---|
Cursor | cursor query | Optional opaque value, maximum 8192 bytes; tied to entity and normalized filters |
Status | status query | Optional active or inactive; omission includes both |
Deletion | deletion query | live, deleted, or all; default live |
Category | category query, history only | Optional; omission includes all 12 categories |
List order is created_at DESC, id DESC. Cursors carry that order position and bind entity/search/category/status/deletion; malformed cursors or filter changes return 422. Counts and page numbers are omitted. Search still matches titles and aliases.
Get/delete DTOs require canonical UUID ID from {id} and no body. Get may return a soft-deleted record for management/recovery visibility. PATCH of a deleted record returns 409. Delete of a missing ID returns 404; deleting an already deleted record succeeds without another mutation/audit event. There is no restore route in this change. A newly created replacement may reuse a deleted record's title with a new ID.
Admin create bodies
Use separate AdminChiefComplaintCreateBodyDto and AdminHistorySuggestionCreateBodyDto types.
Chief complaint example:
{
"title": "Fever",
"aliases": ["জ্বর", "Pyrexia"],
"associated": ["Chills & rigor", "Headache", "Convulsion"],
"status": "active",
"sort_order": 10
}History example:
{
"category": "allergy",
"title": "Penicillin",
"aliases": ["পেনিসিলিন"],
"attributes": {"allergyKind": "Drug"},
"status": "active",
"sort_order": 10
}| Field | Applicable resource | Required/default | Validation |
|---|---|---|---|
title | Both | Required | Nonblank, 1–500 Unicode characters after trimming/whitespace normalization |
category | History | Required | Exact supported category |
aliases | Both | Default [] | At most 50 nonblank strings, each at most 255 characters; no normalized duplicates |
associated | Chief complaint | Default [] | At most 30 nonblank strings, each at most 255 characters; no normalized duplicates; preserve order |
attributes | History | Default {} | Typed object; only allergyKind is currently allowed |
attributes.allergyKind | Allergy history | Optional | Drug, Food, Other, or Unspecified; forbidden for other categories |
status | Both | Default active | active or inactive |
sort_order | Both | Default 0 | Integer 0–1,000,000; smaller values appear first in Doctor defaults |
Reject null for these non-nullable fields, invalid UTF-8/control input, blank array entries, duplicate normalized entries, unknown body keys, and patient-specific attributes. Normalize strings using the same shared rules as search. Preserve Bengali marks and original letter case for display. The normalized title is unique among live chief complaints, or within a history category; conflicts return 409.
The server creates ID/timestamps. Clients cannot supply id, seed_key, normalized search values, timestamps, deletion metadata, actor, or permissions.
Admin PATCH bodies
Use AdminChiefComplaintPatchBodyDto and AdminHistorySuggestionPatchBodyDto with presence-aware fields. The path ID is required. Retain raw body/presence information locally in the HTTP DTO/mapper so omission, zero, empty arrays/objects, and null remain distinguishable.
{
"aliases": ["জ্বর", "Pyrexia", "Raised temperature"],
"associated": [],
"sort_order": 0
}- Omitted fields retain their current values. Arrays and the attributes object replace their entire stored value when supplied.
[]clears aliases or associations;{}clears attributes. Explicitsort_order: 0is applied.- Explicit null, unknown fields, or an empty patch return 422. POST defaults must not be injected into PATCH DTOs.
- Validate the merged record under a row lock. A category change from allergy must clear incompatible attributes in the same patch, for example
{"category":"other","attributes":{}}. - Content, category, status, and ranking changes preserve ID. Update search values and
updated_atin the same transaction. - Repeated no-op patches return the unchanged resource without an extra audit event. Concurrent patches serialize and each patch applies only its supplied fields to the latest row.
4. Response DTO fields and examples
Doctor list item DTOs are DoctorChiefComplaintListItemDto and DoctorHistorySuggestionListItemDto. Their fields match the original requested shapes exactly:
| Resource | Item fields |
|---|---|
| Chief complaint | id, title, aliases: string[], associated: string[] |
| History | id, category, title, aliases: string[], attributes: object |
Admin resource item/data DTOs add status, sort_order, created_at, updated_at, and deleted_at. Always emit deleted_at, using null for live records and an RFC 3339 timestamp for deleted records. Other timestamps are RFC 3339 UTC. Do not expose seed bookkeeping or normalized search values.
Each Admin operation has its own documented response wrapper, body, and list-item/data DTO named with its operation stem, for example AdminChiefComplaintCreateResDto, AdminChiefComplaintCreateResponseBodyDto, and AdminChiefComplaintCreateDataDto. Reuse local nested attributes/metadata DTOs where appropriate, while keeping the domain types free of serialization concerns.
Example Admin create response:
{
"message": "Chief complaint created successfully",
"status_code": 201,
"data": {
"id": "0bde819f-5299-4c87-986a-ed7c1f38e6d7",
"title": "Fever",
"aliases": ["জ্বর", "Pyrexia"],
"associated": ["Chills & rigor", "Headache", "Convulsion"],
"status": "active",
"sort_order": 10,
"created_at": "2026-10-28T06:00:00Z",
"updated_at": "2026-10-28T06:00:00Z",
"deleted_at": null
}
}Example Admin history list:
{
"message": "History suggestions fetched successfully",
"status_code": 200,
"data": [
{
"id": "c39fe285-2daa-439d-a936-c5b1e1af8566",
"category": "allergy",
"title": "Penicillin",
"aliases": ["পেনিসিলিন"],
"attributes": {"allergyKind": "Drug"},
"status": "active",
"sort_order": 10,
"created_at": "2026-10-28T06:00:00Z",
"updated_at": "2026-10-28T06:00:00Z",
"deleted_at": null
}
],
"pagination": {"next_cursor": ""}
}Create/get/patch use their resource's same data fields; only the message and create status differ. Chief complaint success messages are Chief complaints fetched successfully, Chief complaint fetched successfully, Chief complaint created successfully, Chief complaint updated successfully, and Chief complaint deleted successfully. History uses History suggestions fetched successfully, History suggestion fetched successfully, History suggestion created successfully, History suggestion updated successfully, and History suggestion deleted successfully.
All routes use existing Huma error documents. Missing/invalid credentials return 401; inactive/non-admin/insufficient-permission callers return 403; missing Admin records 404; duplicate identity or deleted-edit conflicts 409; invalid requests 422; unavailable dependencies 503; unexpected failures 500. Authentication and authorization errors remain local to Doctor/Admin; the shared provider errors below describe catalog behavior.
5. Shared domain models, queries, and commands
These definitions illustrate the serialization-free package. Add Go comments to every exported type, field, constant, error, and interface method when implementing them.
// HistoryCategory identifies one of the twelve supported history sections.
type HistoryCategory string
// Status controls eligibility for Doctor autocomplete results.
type Status string // active | inactive
// DeletionFilter controls visibility in administrator list queries.
type DeletionFilter string // live | deleted | all
// Metadata contains catalog identity, lifecycle, and display priority.
type Metadata struct {
ID string
Status Status
SortOrder int
CreatedAt, UpdatedAt time.Time
DeletedAt *time.Time
}
// ChiefComplaint is a catalog term with optional associated complaint choices.
type ChiefComplaint struct {
Metadata
Title string
Aliases, Associated []string
}
// HistoryAttributes contains catalog classifications, not patient findings.
type HistoryAttributes struct {
AllergyKind string // empty, Drug, Food, Other, or Unspecified
}
// HistorySuggestion is a history term scoped to one category.
type HistorySuggestion struct {
Metadata
Category HistoryCategory
Title string
Aliases []string
Attributes HistoryAttributes
}
// SuggestionQuery selects a bounded autocomplete result without a cursor.
type SuggestionQuery struct { Search string; Limit int }
// HistorySuggestionQuery adds the required category filter.
type HistorySuggestionQuery struct {
SuggestionQuery
Category HistoryCategory
}
// CatalogQuery selects a filter-bound management page.
type CatalogQuery struct {
Search, Cursor string
Limit int
Status Status
Deletion DeletionFilter
}
// HistoryCatalogQuery optionally limits management results to one category.
type HistoryCatalogQuery struct {
CatalogQuery
Category HistoryCategory // empty means all categories
}
// Page contains a bounded management page and an opaque continuation.
type Page[T any] struct { Items []T; NextCursor string }
// MutationActor identifies the authorized administrator for atomic audit.
// Admin derives it from verified identity/membership, never a request body.
type MutationActor struct { UserID, MembershipID, Name string }
// Field retains PATCH presence independently of a value's zero value.
// HTTP mappers reject explicit null before constructing this domain value.
type Field[T any] struct { Present bool; Value T }
// ChiefComplaintValues supplies validated create values after defaults.
type ChiefComplaintValues struct {
Title string
Aliases, Associated []string
Status Status
SortOrder int
}
// HistoryValues supplies a category and compatible classifications.
type HistoryValues struct {
Category HistoryCategory
Title string
Aliases []string
Attributes HistoryAttributes
Status Status
SortOrder int
}
// ChiefComplaintPatch represents exactly the fields supplied by an editor.
type ChiefComplaintPatch struct {
Title Field[string]
Aliases, Associated Field[[]string]
Status Field[Status]
SortOrder Field[int]
}
// HistoryPatch is validated together with the current stored record.
type HistoryPatch struct {
Category Field[HistoryCategory]
Title Field[string]
Aliases Field[[]string]
Attributes Field[HistoryAttributes]
Status Field[Status]
SortOrder Field[int]
}
// CreateChiefComplaintInput combines authorized attribution and new values.
type CreateChiefComplaintInput struct {
Actor MutationActor
Values ChiefComplaintValues
}
// PatchChiefComplaintInput identifies the record and supplied edits.
type PatchChiefComplaintInput struct {
Actor MutationActor
ID string
Patch ChiefComplaintPatch
}
// CreateHistoryInput combines authorized attribution and new history values.
type CreateHistoryInput struct { Actor MutationActor; Values HistoryValues }
// PatchHistoryInput identifies the history term and supplied edits.
type PatchHistoryInput struct { Actor MutationActor; ID string; Patch HistoryPatch }
// DeleteInput soft-deletes one term while retaining actor attribution.
type DeleteInput struct { Actor MutationActor; ID string }Define typed constants for every supported category, status, deletion filter, and allergy classification. Keep normalization/validation helpers with these shared value objects so seeds and HTTP-backed operations use identical rules. Storage-only seed keys and normalized columns stay in private adapter records.
The domain.CatalogSnapshot and domain.CatalogChange models describe before/after audit values and remain in the domain package.
Stable shared errors: ErrInvalidInput, ErrNotFound, ErrConflict, and ErrUnavailable. Match with errors.Is. List operations never use ErrNotFound for zero matches. Context cancellation/deadline errors propagate so callers can distinguish request cancellation from validation or a missing term. Do not expose raw SQL or internal dependency details over HTTP.
6. Service ports
Service ports take domain values and return domain values. Doctor/Admin authenticate first; these are trusted in-process capabilities, not independent HTTP authorization boundaries. Provider validation still runs for every call. Admin must construct MutationActor from its verified principal and pass request metadata through context.Context; bearer tokens never reach catalog repositories.
package ports
import (
"context"
"github.com/prescriptoo/prescriptoo-api/pkg/types/domain"
)
// DoctorClinicalService supplies active, non-deleted clinical input suggestions.
// Doctor authenticates the persistent session before invoking this query-only service.
type DoctorClinicalService interface {
// SuggestChiefComplaints searches title/aliases or returns curated defaults.
// Results include associations and contain at most query.Limit records.
SuggestChiefComplaints(ctx context.Context, query domain.SuggestionQuery) ([]domain.ChiefComplaint, error)
// SuggestHistory requires a valid category and includes classification attributes.
SuggestHistory(ctx context.Context, query domain.HistorySuggestionQuery) ([]domain.HistorySuggestion, error)
}
// AdminClinicalSuggestionService manages both clinical suggestion catalogs.
// Admin enforces read/manage permissions before invoking each operation. Mutations
// carry verified actor attribution and commit their catalog changes with audit.
type AdminClinicalSuggestionService interface {
// ListChiefComplaints returns a page under the management filters/order.
ListChiefComplaints(ctx context.Context, query domain.CatalogQuery) (domain.Page[domain.ChiefComplaint], error)
// GetChiefComplaint returns one term, including a deleted term, or ErrNotFound.
GetChiefComplaint(ctx context.Context, id string) (domain.ChiefComplaint, error)
// ListHistorySuggestions optionally filters category across a management page.
ListHistorySuggestions(ctx context.Context, query domain.HistoryCatalogQuery) (domain.Page[domain.HistorySuggestion], error)
// GetHistorySuggestion returns one term, including a deleted term, or ErrNotFound.
GetHistorySuggestion(ctx context.Context, id string) (domain.HistorySuggestion, error)
// CreateChiefComplaint validates identity and commits the term with audit.
CreateChiefComplaint(ctx context.Context, input domain.CreateChiefComplaintInput) (domain.ChiefComplaint, error)
// PatchChiefComplaint merges supplied fields under lock and audits actual changes.
PatchChiefComplaint(ctx context.Context, input domain.PatchChiefComplaintInput) (domain.ChiefComplaint, error)
// DeleteChiefComplaint soft-deletes once; an already deleted ID is a no-op.
DeleteChiefComplaint(ctx context.Context, input domain.DeleteInput) error
// CreateHistorySuggestion validates category/attributes and commits with audit.
CreateHistorySuggestion(ctx context.Context, input domain.CreateHistoryInput) (domain.HistorySuggestion, error)
// PatchHistorySuggestion validates the merged category and classification values.
PatchHistorySuggestion(ctx context.Context, input domain.PatchHistoryInput) (domain.HistorySuggestion, error)
// DeleteHistorySuggestion soft-deletes once without changing prior clinical documents.
DeleteHistorySuggestion(ctx context.Context, input domain.DeleteInput) error
}Each implementation file contains one use case. Prescription exposes two separate concrete implementations through Module.DoctorClinicalService() and Module.AdminClinicalSuggestionService(). Doctor receives only the two-query service, and Admin receives the service combining all ten catalog management operations. There are no separate reader/writer service interfaces. Admin permission checks and Doctor login checks remain in their owning services.
7. Repository and transaction ports
Keep repository interfaces in pkg/types/ports, implemented by Prescription's PostgreSQL adapters. Queries return initialized collections and obey the service query semantics. They are internal implementation dependencies and are not injected into Doctor/Admin.
package ports
import (
"context"
"github.com/prescriptoo/prescriptoo-api/pkg/types/domain"
)
// ChiefComplaintRepository provides catalog reads without transport concerns.
type ChiefComplaintRepository interface {
// Suggest performs matching/ranking and includes inline associations.
Suggest(ctx context.Context, query domain.SuggestionQuery) ([]domain.ChiefComplaint, error)
// List applies management filtering and cursor pagination.
List(ctx context.Context, query domain.CatalogQuery) (domain.Page[domain.ChiefComplaint], error)
// Get returns a term irrespective of its deletion state.
Get(ctx context.Context, id string) (domain.ChiefComplaint, error)
}
// HistoryRepository provides categorized catalog reads.
type HistoryRepository interface {
// Suggest enforces category and active/live eligibility before limiting.
Suggest(ctx context.Context, query domain.HistorySuggestionQuery) ([]domain.HistorySuggestion, error)
// List supports optional category and lifecycle filters for administrators.
List(ctx context.Context, query domain.HistoryCatalogQuery) (domain.Page[domain.HistorySuggestion], error)
// Get returns a history term irrespective of its deletion state.
Get(ctx context.Context, id string) (domain.HistorySuggestion, error)
}
// MutationStore commits catalog writes and audit in one storage transaction.
type MutationStore interface {
// WithinMutation rolls back on callback/audit/commit failure and honors cancellation.
// The callback receives domain interfaces, never a database-driver transaction.
WithinMutation(ctx context.Context, actor domain.MutationActor, fn func(Mutation) error) error
}
// Mutation is valid only for the lifetime of a WithinMutation callback.
type Mutation interface {
// LockChiefComplaint reads current state under a write lock, including deleted rows.
LockChiefComplaint(ctx context.Context, id string) (domain.ChiefComplaint, error)
// InsertChiefComplaint enforces ID and live-title uniqueness.
InsertChiefComplaint(ctx context.Context, item domain.ChiefComplaint) error
// SaveChiefComplaint persists validated edits/deletion plus normalized search values.
SaveChiefComplaint(ctx context.Context, item domain.ChiefComplaint) error
// LockHistorySuggestion reads the latest history term under a write lock.
LockHistorySuggestion(ctx context.Context, id string) (domain.HistorySuggestion, error)
// InsertHistorySuggestion enforces category-scoped live-title uniqueness.
InsertHistorySuggestion(ctx context.Context, item domain.HistorySuggestion) error
// SaveHistorySuggestion persists merged content/lifecycle/search values.
SaveHistorySuggestion(ctx context.Context, item domain.HistorySuggestion) error
// AppendAudit appends a domain change through the transaction-bound audit adapter.
AppendAudit(ctx context.Context, change domain.CatalogChange) error
}The transaction adapter creates the existing Audit operation context using its verified MutationActor, fixed Admin scope, and request metadata. Create has an empty before snapshot. Soft delete has a before and deleted after snapshot. A no-op PATCH/DELETE produces no additional event. If an audit append fails, the catalog write rolls back. Shared contracts must not import the Audit service package to express this behavior.
The provider chooses the row ID and timestamps. Adapters translate database uniqueness/missing/storage failures to shared errors. Keep seed bookkeeping private and preserve it during every Admin save; domain models cannot overwrite it accidentally. Every Admin edit/delete and seed run must coordinate on the catalog transaction lock where seed identity is involved so a seed cannot recreate or overwrite an edited/deleted term.
8. Required documentation and verification
When implementing, add substantive Go comments to each ReqDto/ResDto, body/data/item DTO, field, mapper, shared model, input, port, and method. Describe source, required/default behavior, empty/null semantics, collection replacement, ordering, errors, and authentication/permission requirements where relevant. Huma doc/validation tags must produce the same contract in runtime/generated OpenAPI.
Verify examples and schemas for all 12 operations. Test DTO-to-domain and domain-to-DTO mappings for Bengali aliases, empty arrays/objects, PATCH field presence/zero values, hidden credentials, metadata, category/attribute compatibility, and delete envelopes. Add boundary checks proving models are in the shared domain package and service/repository interfaces are in ports, domain types have no HTTP serialization tags, and no new definitions are added under services/prescription/public.
This contract supersedes the original plan's Prescription-public interfaces and seed-owned overwrite behavior. All previously confirmed language, history-category, legacy-data recovery, and Doctor-only-read decisions remain in effect.