From 3bdc27f4de4c73168b85e06de786226dbfb3b0d5 Mon Sep 17 00:00:00 2001 From: Jay Brown Date: Tue, 2 Dec 2025 19:13:08 +0000 Subject: [PATCH] Merged in feature/textExtractionsPart2 (pull request #193) Continue finishing the parts of the text extraction plan * add missing fields --- api/queryAPI/api.gen.go | 520 +++++++++++------- api/queryAPI/client.go | 24 + api/queryAPI/client_test.go | 73 +++ api/queryAPI/fieldextractions.go | 241 ++++++++ api/queryAPI/fieldextractions_test.go | 187 +++++++ internal/client/list.go | 20 + ...117_add_version_unique_constraint.down.sql | 10 + ...00117_add_version_unique_constraint.up.sql | 30 + internal/database/queries/client.sql | 4 + .../database/queries/fieldextractions.sql | 54 +- internal/database/repository/client.sql.go | 27 + .../repository/fieldextractions.sql.go | 201 ++++++- internal/database/repository/models.go | 1 + internal/fieldextraction/models.go | 36 +- internal/fieldextraction/models_test.go | 111 ++++ internal/fieldextraction/service.go | 53 +- mocks/queryapi/mock_ClientInterface.go | 147 +++++ .../mock_ClientWithResponsesInterface.go | 147 +++++ pkg/queryAPI/api.gen.go | 372 +++++++++++++ serviceAPIs/queryAPI.yaml | 279 ++++++++++ test/text_extraction_integration_test.go | 70 ++- 21 files changed, 2392 insertions(+), 215 deletions(-) create mode 100644 internal/client/list.go create mode 100644 internal/database/migrations/00000000000117_add_version_unique_constraint.down.sql create mode 100644 internal/database/migrations/00000000000117_add_version_unique_constraint.up.sql create mode 100644 internal/fieldextraction/models_test.go diff --git a/api/queryAPI/api.gen.go b/api/queryAPI/api.gen.go index 35f707db..29ba0e65 100644 --- a/api/queryAPI/api.gen.go +++ b/api/queryAPI/api.gen.go @@ -248,6 +248,7 @@ type ArrayFieldItem struct { AareteDerivedClaimTypeCd *string `json:"aareteDerivedClaimTypeCd,omitempty"` AareteDerivedFeeSchedule *string `json:"aareteDerivedFeeSchedule,omitempty"` AareteDerivedFeeScheduleVersion *string `json:"aareteDerivedFeeScheduleVersion,omitempty"` + AareteDerivedGrouperVersion *string `json:"aareteDerivedGrouperVersion,omitempty"` AareteDerivedLob *string `json:"aareteDerivedLob,omitempty"` AareteDerivedNetwork *string `json:"aareteDerivedNetwork,omitempty"` AareteDerivedProduct *string `json:"aareteDerivedProduct,omitempty"` @@ -272,19 +273,48 @@ type ArrayFieldItem struct { DefaultInd *bool `json:"defaultInd,omitempty"` DiagCd *string `json:"diagCd,omitempty"` DiagCdDesc *string `json:"diagCdDesc,omitempty"` + DshFeeRate *float64 `json:"dshFeeRate,omitempty"` + DshInd *bool `json:"dshInd,omitempty"` + DshPctRate *float64 `json:"dshPctRate,omitempty"` ExhibitPage *string `json:"exhibitPage,omitempty"` ExhibitTitle *string `json:"exhibitTitle,omitempty"` + FacilityAdjustmentTerm *string `json:"facilityAdjustmentTerm,omitempty"` + GmeFeeRate *float64 `json:"gmeFeeRate,omitempty"` + GmeInd *bool `json:"gmeInd,omitempty"` + GmePctRate *float64 `json:"gmePctRate,omitempty"` GreaterOfInd *bool `json:"greaterOfInd,omitempty"` + GrouperAlternativeLevelOfCare *string `json:"grouperAlternativeLevelOfCare,omitempty"` GrouperBaseRate *float64 `json:"grouperBaseRate,omitempty"` GrouperCd *string `json:"grouperCd,omitempty"` GrouperCdDesc *string `json:"grouperCdDesc,omitempty"` + GrouperHacInd *bool `json:"grouperHacInd,omitempty"` GrouperPctRate *float64 `json:"grouperPctRate,omitempty"` + GrouperReadmissionsInd *bool `json:"grouperReadmissionsInd,omitempty"` + GrouperRiskOfMortalitySubclass *string `json:"grouperRiskOfMortalitySubclass,omitempty"` + GrouperSeverity *string `json:"grouperSeverity,omitempty"` + GrouperSeverityInd *bool `json:"grouperSeverityInd,omitempty"` + GrouperTransferInd *bool `json:"grouperTransferInd,omitempty"` GrouperType *string `json:"grouperType,omitempty"` + ImeFeeRate *float64 `json:"imeFeeRate,omitempty"` + ImeInd *bool `json:"imeInd,omitempty"` + ImePctRate *float64 `json:"imePctRate,omitempty"` LesserOfInd *bool `json:"lesserOfInd,omitempty"` LobProductRelationship *string `json:"lobProductRelationship,omitempty"` LobProgramRelationship *string `json:"lobProgramRelationship,omitempty"` NdcCd *string `json:"ndcCd,omitempty"` NdcCdDesc *string `json:"ndcCdDesc,omitempty"` + NtapFeeRate *float64 `json:"ntapFeeRate,omitempty"` + NtapInd *bool `json:"ntapInd,omitempty"` + NtapPctRate *float64 `json:"ntapPctRate,omitempty"` + OutlierExclusionCd *string `json:"outlierExclusionCd,omitempty"` + OutlierExclusionCdDesc *string `json:"outlierExclusionCdDesc,omitempty"` + OutlierFirstDollarInd *bool `json:"outlierFirstDollarInd,omitempty"` + OutlierFixedLossNbrDaysThreshold *float64 `json:"outlierFixedLossNbrDaysThreshold,omitempty"` + OutlierFixedLossThreshold *float64 `json:"outlierFixedLossThreshold,omitempty"` + OutlierMaximum *float64 `json:"outlierMaximum,omitempty"` + OutlierMaximumFrequency *float64 `json:"outlierMaximumFrequency,omitempty"` + OutlierPctRate *float64 `json:"outlierPctRate,omitempty"` + OutlierTerm *string `json:"outlierTerm,omitempty"` PatientAgeMax *string `json:"patientAgeMax,omitempty"` PatientAgeMin *string `json:"patientAgeMin,omitempty"` PlaceOfServiceCd *string `json:"placeOfServiceCd,omitempty"` @@ -293,6 +323,11 @@ type ArrayFieldItem struct { ProvSpecialtyCdDesc *string `json:"provSpecialtyCdDesc,omitempty"` ProvTaxonomyCd *string `json:"provTaxonomyCd,omitempty"` ProvTaxonomyCdDesc *string `json:"provTaxonomyCdDesc,omitempty"` + RangeNbrDays *string `json:"rangeNbrDays,omitempty"` + RateEscalatorDesc *string `json:"rateEscalatorDesc,omitempty"` + RateEscalatorInd *bool `json:"rateEscalatorInd,omitempty"` + RateEscalatorMaxRateIncPct *float64 `json:"rateEscalatorMaxRateIncPct,omitempty"` + RateEscalatorRateChangeTimeline *float64 `json:"rateEscalatorRateChangeTimeline,omitempty"` ReimbConversionFactor *float64 `json:"reimbConversionFactor,omitempty"` ReimbEffectiveDt *openapi_types.Date `json:"reimbEffectiveDt,omitempty"` ReimbFeeRate *float64 `json:"reimbFeeRate,omitempty"` @@ -305,8 +340,21 @@ type ArrayFieldItem struct { RevenueCd *string `json:"revenueCd,omitempty"` RevenueCdDesc *string `json:"revenueCdDesc,omitempty"` ServiceTerm *string `json:"serviceTerm,omitempty"` + StopLossDailyMaxRate *float64 `json:"stopLossDailyMaxRate,omitempty"` + StopLossExclusionCd *string `json:"stopLossExclusionCd,omitempty"` + StopLossExclusionDesc *string `json:"stopLossExclusionDesc,omitempty"` + StopLossFirstDollarInd *bool `json:"stopLossFirstDollarInd,omitempty"` + StopLossFixedLossThreshold *float64 `json:"stopLossFixedLossThreshold,omitempty"` + StopLossMaximum *float64 `json:"stopLossMaximum,omitempty"` + StopLossMaximumFrequency *float64 `json:"stopLossMaximumFrequency,omitempty"` + StopLossPctRateOnExcessCharges *float64 `json:"stopLossPctRateOnExcessCharges,omitempty"` + StopLossRangeNbrDays *float64 `json:"stopLossRangeNbrDays,omitempty"` + StopLossTerm *string `json:"stopLossTerm,omitempty"` TriggerBaseThreshold *float64 `json:"triggerBaseThreshold,omitempty"` TriggerCapThresholdAmt *float64 `json:"triggerCapThresholdAmt,omitempty"` + UcFeeRate *float64 `json:"ucFeeRate,omitempty"` + UcInd *bool `json:"ucInd,omitempty"` + UcPctRate *float64 `json:"ucPctRate,omitempty"` UnitOfMeasure *string `json:"unitOfMeasure,omitempty"` } @@ -432,6 +480,15 @@ type ClientIDBody struct { Id ClientID `json:"id"` } +// ClientListResponse Response containing a list of all clients. +type ClientListResponse struct { + // Clients List of clients in the system. + Clients []DocClient `json:"clients"` + + // TotalCount Total number of clients. + TotalCount int32 `json:"totalCount"` +} + // ClientName The client name type ClientName = string @@ -995,6 +1052,15 @@ type GetFieldExtractionHistoryParams struct { DocumentId openapi_types.UUID `form:"documentId" json:"documentId"` } +// GetFieldExtractionByVersionParams defines parameters for GetFieldExtractionByVersion. +type GetFieldExtractionByVersionParams struct { + // DocumentId The document ID + DocumentId openapi_types.UUID `form:"documentId" json:"documentId"` + + // Version The version number to retrieve + Version int64 `form:"version" json:"version"` +} + // GetDocumentsByLabelParams defines parameters for GetDocumentsByLabel. type GetDocumentsByLabelParams struct { // ClientId The client ID to filter documents @@ -1123,6 +1189,9 @@ type ServerInterface interface { // Get client sync status // (GET /client/{id}/status) GetStatusByClientId(ctx echo.Context, id ClientID) error + // List all clients + // (GET /clients) + ListClients(ctx echo.Context) error // Get document details by its id // (GET /document/{id}) GetDocument(ctx echo.Context, id DocumentID) error @@ -1144,6 +1213,9 @@ type ServerInterface interface { // Get field extraction version history // (GET /field-extractions/history) GetFieldExtractionHistory(ctx echo.Context, params GetFieldExtractionHistoryParams) error + // Get field extraction by version + // (GET /field-extractions/version) + GetFieldExtractionByVersion(ctx echo.Context, params GetFieldExtractionByVersionParams) error // Create a new folder // (POST /folders) CreateFolder(ctx echo.Context) error @@ -1653,6 +1725,17 @@ func (w *ServerInterfaceWrapper) GetStatusByClientId(ctx echo.Context) error { return err } +// ListClients converts echo context to params. +func (w *ServerInterfaceWrapper) ListClients(ctx echo.Context) error { + var err error + + ctx.Set(JwtAuthScopes, []string{}) + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.ListClients(ctx) + return err +} + // GetDocument converts echo context to params. func (w *ServerInterfaceWrapper) GetDocument(ctx echo.Context) error { var err error @@ -1776,6 +1859,33 @@ func (w *ServerInterfaceWrapper) GetFieldExtractionHistory(ctx echo.Context) err return err } +// GetFieldExtractionByVersion converts echo context to params. +func (w *ServerInterfaceWrapper) GetFieldExtractionByVersion(ctx echo.Context) error { + var err error + + ctx.Set(JwtAuthScopes, []string{}) + + // Parameter object where we will unmarshal all parameters from the context + var params GetFieldExtractionByVersionParams + // ------------- Required query parameter "documentId" ------------- + + err = runtime.BindQueryParameter("form", true, true, "documentId", ctx.QueryParams(), ¶ms.DocumentId) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter documentId: %s", err)) + } + + // ------------- Required query parameter "version" ------------- + + err = runtime.BindQueryParameter("form", true, true, "version", ctx.QueryParams(), ¶ms.Version) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid format for parameter version: %s", err)) + } + + // Invoke the callback with all the unmarshaled arguments + err = w.Handler.GetFieldExtractionByVersion(ctx, params) + return err +} + // CreateFolder converts echo context to params. func (w *ServerInterfaceWrapper) CreateFolder(ctx echo.Context) error { var err error @@ -2052,6 +2162,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.POST(baseURL+"/client/:id/export", wrapper.TriggerExport) router.GET(baseURL+"/client/:id/folders", wrapper.ListClientFolders) router.GET(baseURL+"/client/:id/status", wrapper.GetStatusByClientId) + router.GET(baseURL+"/clients", wrapper.ListClients) router.GET(baseURL+"/document/:id", wrapper.GetDocument) router.GET(baseURL+"/documents/:documentId/labels", wrapper.GetDocumentLabels) router.POST(baseURL+"/documents/:documentId/labels", wrapper.ApplyLabel) @@ -2059,6 +2170,7 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL router.GET(baseURL+"/field-extractions", wrapper.GetCurrentFieldExtraction) router.POST(baseURL+"/field-extractions", wrapper.CreateFieldExtraction) router.GET(baseURL+"/field-extractions/history", wrapper.GetFieldExtractionHistory) + router.GET(baseURL+"/field-extractions/version", wrapper.GetFieldExtractionByVersion) router.POST(baseURL+"/folders", wrapper.CreateFolder) router.PATCH(baseURL+"/folders/:folderId", wrapper.RenameFolder) router.GET(baseURL+"/folders/:folderId/documents", wrapper.GetFolderDocuments) @@ -2079,205 +2191,215 @@ func RegisterHandlersWithBaseURL(router EchoRouter, si ServerInterface, baseURL // Base64 encoded, gzipped, json marshaled Swagger object var swaggerSpec = []string{ - "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", + "mArJi+O7lcm/fvj6/wIAAP//vFXFz1tGAQA=", } // GetSwagger returns the content of the embedded swagger specification file diff --git a/api/queryAPI/client.go b/api/queryAPI/client.go index 32397c31..5ebe8003 100644 --- a/api/queryAPI/client.go +++ b/api/queryAPI/client.go @@ -58,3 +58,27 @@ func (s *Controllers) UpdateClient(ctx echo.Context, id ClientID) error { return ctx.NoContent(http.StatusOK) } + +// ListClients returns all clients in the system. +func (s *Controllers) ListClients(ctx echo.Context) error { + clients, err := s.svc.Client.List(ctx.Request().Context()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, fmt.Sprintf("Unable to list clients: %s", err)) + } + + // Convert to API response format + apiClients := make([]DocClient, len(clients)) + for i, c := range clients { + apiClients[i] = DocClient{ + Id: c.ID, + Name: c.Name, + CanSync: c.CanSync, + } + } + + return ctx.JSON(http.StatusOK, ClientListResponse{ + Clients: apiClients, + //nolint:gosec // Client count is bounded by maxItems: 10000 in OpenAPI spec + TotalCount: int32(len(apiClients)), + }) +} diff --git a/api/queryAPI/client_test.go b/api/queryAPI/client_test.go index 99edd7cf..26c1a71c 100644 --- a/api/queryAPI/client_test.go +++ b/api/queryAPI/client_test.go @@ -111,6 +111,79 @@ func TestUpdateClient(t *testing.T) { assert.Equal(t, "new_name", client.Name) } +func TestListClients(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + // Create multiple clients + clients := []struct { + id string + name string + }{ + {"client_a", "Alpha Client"}, + {"client_b", "Beta Client"}, + {"client_c", "Charlie Client"}, + } + + for _, c := range clients { + err := cfg.GetDBQueries().CreateClient(t.Context(), &repository.CreateClientParams{ + Clientid: c.id, + Name: c.name, + }) + require.NoError(t, err) + err = cfg.GetDBQueries().AddClientCanSync(t.Context(), &repository.AddClientCanSyncParams{ + Clientid: c.id, + Cansync: true, + }) + require.NoError(t, err) + } + + ctx, rec := createContext(t) + + err := cons.ListClients(ctx) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + var response queryapi.ClientListResponse + err = json.Unmarshal(rec.Body.Bytes(), &response) + require.NoError(t, err) + + // Verify response + assert.Equal(t, int32(3), response.TotalCount) + assert.Len(t, response.Clients, 3) + + // Verify clients are ordered by name (Alpha, Beta, Charlie) + assert.Equal(t, "Alpha Client", response.Clients[0].Name) + assert.Equal(t, "Beta Client", response.Clients[1].Name) + assert.Equal(t, "Charlie Client", response.Clients[2].Name) +} + +func TestListClientsEmpty(t *testing.T) { + cfg := &ControllerConfig{} + test.CreateDB(t, cfg) + initializeTestConfig(t, cfg) + + svc := createControllerServices(cfg) + cons := queryapi.NewControllers(svc, cfg) + + ctx, rec := createContext(t) + + err := cons.ListClients(ctx) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + var response queryapi.ClientListResponse + err = json.Unmarshal(rec.Body.Bytes(), &response) + require.NoError(t, err) + + assert.Equal(t, int32(0), response.TotalCount) + assert.Empty(t, response.Clients) +} + func initializeTestConfig(t testing.TB, cfg *ControllerConfig) { // Reset the singleton config state for testing serviceconfig.ResetConfigInitOnceTestOnly() diff --git a/api/queryAPI/fieldextractions.go b/api/queryAPI/fieldextractions.go index b3bb0ea2..8883a9dd 100644 --- a/api/queryAPI/fieldextractions.go +++ b/api/queryAPI/fieldextractions.go @@ -141,6 +141,42 @@ func (s *Controllers) GetFieldExtractionHistory(ctx echo.Context, params GetFiel }) } +// GetFieldExtractionByVersion retrieves a specific version of field extraction for a document. +// This endpoint returns the full field extraction data for a specific version. +func (s *Controllers) GetFieldExtractionByVersion(ctx echo.Context, params GetFieldExtractionByVersionParams) error { + documentID := uuid.UUID(params.DocumentId) + if documentID == uuid.Nil { + return echo.NewHTTPError(http.StatusBadRequest, "documentId is required") + } + + if params.Version < 1 { + return echo.NewHTTPError(http.StatusBadRequest, "version must be at least 1") + } + + // Get field extraction by version + extraction, err := s.svc.FieldExtraction.GetFieldExtractionByVersion(ctx.Request().Context(), documentID, params.Version) + if err != nil { + slog.Error("failed to get field extraction by version", "error", err, "document_id", documentID, "version", params.Version) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get field extraction") + } + + if extraction == nil { + return echo.NewHTTPError(http.StatusNotFound, fmt.Sprintf("no field extraction found for document at version %d", params.Version)) + } + + // Fetch array fields + arrayFields, err := s.svc.FieldExtraction.GetFieldExtractionArrayFields(ctx.Request().Context(), extraction.ID) + if err != nil { + slog.Error("failed to get array fields", "error", err, "extraction_id", extraction.ID) + return echo.NewHTTPError(http.StatusInternalServerError, "failed to get array fields") + } + + // Build response + response := buildFieldExtractionResponseFromVersionRow(extraction, arrayFields) + + return ctx.JSON(http.StatusOK, response) +} + // buildFieldExtractionResponse builds the API response from the repository types func buildFieldExtractionResponse(current *repository.Currentfieldextraction, arrayFields []*repository.Documentfieldextractionarrayfield) *FieldExtractionResponse { createdAt := time.Time{} @@ -160,6 +196,59 @@ func buildFieldExtractionResponse(current *repository.Currentfieldextraction, ar } } +// buildFieldExtractionResponseFromVersionRow builds the API response from the version-specific repository types +func buildFieldExtractionResponseFromVersionRow(row *repository.GetFieldExtractionByVersionRow, arrayFields []*repository.Documentfieldextractionarrayfield) *FieldExtractionResponse { + createdAt := time.Time{} + if row.Createdat.Valid { + createdAt = row.Createdat.Time + } + + return &FieldExtractionResponse{ + Id: openapi_types.UUID(row.ID), + DocumentId: DocumentID(row.Documentid), + //nolint:gosec // Version is a small number, int32 is sufficient + Version: int32(row.Version), // Already 1-based in database + SingleFields: convertRepoSingleFieldsToAPIFromVersionRow(row), + ArrayFields: convertRepoArrayFieldsToAPI(arrayFields), + CreatedAt: createdAt, + CreatedBy: openapi_types.Email(row.Createdby), + } +} + +// convertRepoSingleFieldsToAPIFromVersionRow converts repository GetFieldExtractionByVersionRow to API SingleFields +func convertRepoSingleFieldsToAPIFromVersionRow(row *repository.GetFieldExtractionByVersionRow) SingleFields { + sf := SingleFields{ + FileName: row.Filename, + ContractTitle: row.Contracttitle, + AareteDerivedAmendmentNum: row.Aaretederivedamendmentnum, + ClientName: row.Clientname, + PayerName: row.Payername, + PayerState: row.Payerstate, + ProviderState: row.Providerstate, + FilenameTin: row.Filenametin, + ProvGroupTin: row.Provgrouptin, + ProvGroupNpi: row.Provgroupnpi, + ProvGroupNameFull: row.Provgroupnamefull, + ProvOtherTin: row.Provothertin, + ProvOtherNpi: row.Provothernpi, + ProvOtherNameFull: row.Provothernamefull, + AutoRenewalInd: row.Autorenewalind, + AutoRenewalTerm: row.Autorenewalterm, + } + + // Convert dates + if row.Aaretederivedeffectivedt.Valid { + d := pgDateToAPIDate(row.Aaretederivedeffectivedt) + sf.AareteDerivedEffectiveDt = &d + } + if row.Aaretederivedterminationdt.Valid { + d := pgDateToAPIDate(row.Aaretederivedterminationdt) + sf.AareteDerivedTerminationDt = &d + } + + return sf +} + // convertAPISingleFieldsToRepo converts API SingleFields to repository AddFieldExtractionParams func convertAPISingleFieldsToRepo(sf *SingleFields, createdBy string) *repository.AddFieldExtractionParams { params := &repository.AddFieldExtractionParams{ @@ -300,12 +389,57 @@ func convertAPIArrayFieldItemToRepo(item *ArrayFieldItem) *repository.AddFieldEx params.Groupercddesc = item.GrouperCdDesc params.Carveoutcd = item.CarveoutCd + // Grouper extended fields + params.Aaretederivedgrouperversion = item.AareteDerivedGrouperVersion + params.Grouperalternativelevelofcare = item.GrouperAlternativeLevelOfCare + params.Grouperseverity = item.GrouperSeverity + params.Grouperriskofmortalitysubclass = item.GrouperRiskOfMortalitySubclass + + // Outlier fields + params.Outlierterm = item.OutlierTerm + params.Rangenbrdays = item.RangeNbrDays + params.Outlierexclusioncd = item.OutlierExclusionCd + params.Outlierexclusioncddesc = item.OutlierExclusionCdDesc + + // Facility adjustment fields + params.Facilityadjustmentterm = item.FacilityAdjustmentTerm + + // Rate escalator fields + params.Rateescalatordesc = item.RateEscalatorDesc + + // Stop loss fields + params.Stoplossterm = item.StopLossTerm + params.Stoplossexclusioncd = item.StopLossExclusionCd + params.Stoplossexclusiondesc = item.StopLossExclusionDesc + // Boolean fields params.Carveoutind = item.CarveoutInd params.Lesserofind = item.LesserOfInd params.Greaterofind = item.GreaterOfInd params.Defaultind = item.DefaultInd + // Grouper extended boolean fields + params.Grouperseverityind = item.GrouperSeverityInd + params.Groupertransferind = item.GrouperTransferInd + params.Grouperreadmissionsind = item.GrouperReadmissionsInd + params.Grouperhacind = item.GrouperHacInd + + // Outlier boolean fields + params.Outlierfirstdollarind = item.OutlierFirstDollarInd + + // Facility adjustment boolean fields + params.Dshind = item.DshInd + params.Imeind = item.ImeInd + params.Ntapind = item.NtapInd + params.Ucind = item.UcInd + params.Gmeind = item.GmeInd + + // Rate escalator boolean fields + params.Rateescalatorind = item.RateEscalatorInd + + // Stop loss boolean fields + params.Stoplossfirstdollarind = item.StopLossFirstDollarInd + // Numeric fields params.Reimbpctrate = float64ToPgNumeric(item.ReimbPctRate) params.Reimbfeerate = float64ToPgNumeric(item.ReimbFeeRate) @@ -317,6 +451,37 @@ func convertAPIArrayFieldItemToRepo(item *ArrayFieldItem) *repository.AddFieldEx params.Grouperpctrate = float64ToPgNumeric(item.GrouperPctRate) params.Grouperbaserate = float64ToPgNumeric(item.GrouperBaseRate) + // Outlier numeric fields + params.Outlierfixedlossnbrdaysthreshold = float64ToPgNumeric(item.OutlierFixedLossNbrDaysThreshold) + params.Outlierfixedlossthreshold = float64ToPgNumeric(item.OutlierFixedLossThreshold) + params.Outliermaximum = float64ToPgNumeric(item.OutlierMaximum) + params.Outliermaximumfrequency = float64ToPgNumeric(item.OutlierMaximumFrequency) + params.Outlierpctrate = float64ToPgNumeric(item.OutlierPctRate) + + // Facility adjustment numeric fields + params.Dshpctrate = float64ToPgNumeric(item.DshPctRate) + params.Dshfeerate = float64ToPgNumeric(item.DshFeeRate) + params.Imepctrate = float64ToPgNumeric(item.ImePctRate) + params.Imefeerate = float64ToPgNumeric(item.ImeFeeRate) + params.Ntappctrate = float64ToPgNumeric(item.NtapPctRate) + params.Ntapfeerate = float64ToPgNumeric(item.NtapFeeRate) + params.Ucpctrate = float64ToPgNumeric(item.UcPctRate) + params.Ucfeerate = float64ToPgNumeric(item.UcFeeRate) + params.Gmepctrate = float64ToPgNumeric(item.GmePctRate) + params.Gmefeerate = float64ToPgNumeric(item.GmeFeeRate) + + // Rate escalator numeric fields + params.Rateescalatormaxrateincpct = float64ToPgNumeric(item.RateEscalatorMaxRateIncPct) + params.Rateescalatorratechangetimeline = float64ToPgNumeric(item.RateEscalatorRateChangeTimeline) + + // Stop loss numeric fields + params.Stoplossrangenbrdays = float64ToPgNumeric(item.StopLossRangeNbrDays) + params.Stoplossfixedlossthreshold = float64ToPgNumeric(item.StopLossFixedLossThreshold) + params.Stoplossmaximum = float64ToPgNumeric(item.StopLossMaximum) + params.Stoplossmaximumfrequency = float64ToPgNumeric(item.StopLossMaximumFrequency) + params.Stoplossdailymaxrate = float64ToPgNumeric(item.StopLossDailyMaxRate) + params.Stoplosspctrateonexcesscharges = float64ToPgNumeric(item.StopLossPctRateOnExcessCharges) + // Date fields if item.ReimbEffectiveDt != nil { params.Reimbeffectivedt = dateToPgDate(*item.ReimbEffectiveDt) @@ -391,11 +556,56 @@ func convertRepoArrayFieldToAPI(f *repository.Documentfieldextractionarrayfield) GrouperCdDesc: f.Groupercddesc, CarveoutCd: f.Carveoutcd, + // Grouper extended string fields + AareteDerivedGrouperVersion: f.Aaretederivedgrouperversion, + GrouperAlternativeLevelOfCare: f.Grouperalternativelevelofcare, + GrouperSeverity: f.Grouperseverity, + GrouperRiskOfMortalitySubclass: f.Grouperriskofmortalitysubclass, + + // Outlier string fields + OutlierTerm: f.Outlierterm, + RangeNbrDays: f.Rangenbrdays, + OutlierExclusionCd: f.Outlierexclusioncd, + OutlierExclusionCdDesc: f.Outlierexclusioncddesc, + + // Facility adjustment string fields + FacilityAdjustmentTerm: f.Facilityadjustmentterm, + + // Rate escalator string fields + RateEscalatorDesc: f.Rateescalatordesc, + + // Stop loss string fields + StopLossTerm: f.Stoplossterm, + StopLossExclusionCd: f.Stoplossexclusioncd, + StopLossExclusionDesc: f.Stoplossexclusiondesc, + // Boolean fields CarveoutInd: f.Carveoutind, LesserOfInd: f.Lesserofind, GreaterOfInd: f.Greaterofind, DefaultInd: f.Defaultind, + + // Grouper extended boolean fields + GrouperSeverityInd: f.Grouperseverityind, + GrouperTransferInd: f.Groupertransferind, + GrouperReadmissionsInd: f.Grouperreadmissionsind, + GrouperHacInd: f.Grouperhacind, + + // Outlier boolean fields + OutlierFirstDollarInd: f.Outlierfirstdollarind, + + // Facility adjustment boolean fields + DshInd: f.Dshind, + ImeInd: f.Imeind, + NtapInd: f.Ntapind, + UcInd: f.Ucind, + GmeInd: f.Gmeind, + + // Rate escalator boolean fields + RateEscalatorInd: f.Rateescalatorind, + + // Stop loss boolean fields + StopLossFirstDollarInd: f.Stoplossfirstdollarind, } // Numeric fields @@ -409,6 +619,37 @@ func convertRepoArrayFieldToAPI(f *repository.Documentfieldextractionarrayfield) item.GrouperPctRate = pgNumericToFloat64(f.Grouperpctrate) item.GrouperBaseRate = pgNumericToFloat64(f.Grouperbaserate) + // Outlier numeric fields + item.OutlierFixedLossNbrDaysThreshold = pgNumericToFloat64(f.Outlierfixedlossnbrdaysthreshold) + item.OutlierFixedLossThreshold = pgNumericToFloat64(f.Outlierfixedlossthreshold) + item.OutlierMaximum = pgNumericToFloat64(f.Outliermaximum) + item.OutlierMaximumFrequency = pgNumericToFloat64(f.Outliermaximumfrequency) + item.OutlierPctRate = pgNumericToFloat64(f.Outlierpctrate) + + // Facility adjustment numeric fields + item.DshPctRate = pgNumericToFloat64(f.Dshpctrate) + item.DshFeeRate = pgNumericToFloat64(f.Dshfeerate) + item.ImePctRate = pgNumericToFloat64(f.Imepctrate) + item.ImeFeeRate = pgNumericToFloat64(f.Imefeerate) + item.NtapPctRate = pgNumericToFloat64(f.Ntappctrate) + item.NtapFeeRate = pgNumericToFloat64(f.Ntapfeerate) + item.UcPctRate = pgNumericToFloat64(f.Ucpctrate) + item.UcFeeRate = pgNumericToFloat64(f.Ucfeerate) + item.GmePctRate = pgNumericToFloat64(f.Gmepctrate) + item.GmeFeeRate = pgNumericToFloat64(f.Gmefeerate) + + // Rate escalator numeric fields + item.RateEscalatorMaxRateIncPct = pgNumericToFloat64(f.Rateescalatormaxrateincpct) + item.RateEscalatorRateChangeTimeline = pgNumericToFloat64(f.Rateescalatorratechangetimeline) + + // Stop loss numeric fields + item.StopLossRangeNbrDays = pgNumericToFloat64(f.Stoplossrangenbrdays) + item.StopLossFixedLossThreshold = pgNumericToFloat64(f.Stoplossfixedlossthreshold) + item.StopLossMaximum = pgNumericToFloat64(f.Stoplossmaximum) + item.StopLossMaximumFrequency = pgNumericToFloat64(f.Stoplossmaximumfrequency) + item.StopLossDailyMaxRate = pgNumericToFloat64(f.Stoplossdailymaxrate) + item.StopLossPctRateOnExcessCharges = pgNumericToFloat64(f.Stoplosspctrateonexcesscharges) + // Date fields if f.Reimbeffectivedt.Valid { d := pgDateToAPIDate(f.Reimbeffectivedt) diff --git a/api/queryAPI/fieldextractions_test.go b/api/queryAPI/fieldextractions_test.go index eff548cd..ac562d60 100644 --- a/api/queryAPI/fieldextractions_test.go +++ b/api/queryAPI/fieldextractions_test.go @@ -16,6 +16,7 @@ import ( "queryorchestration/internal/test" "github.com/google/uuid" + "github.com/jackc/pgx/v5/pgtype" "github.com/labstack/echo/v4" openapi_types "github.com/oapi-codegen/runtime/types" "github.com/stretchr/testify/assert" @@ -639,6 +640,192 @@ func TestCreateFieldExtraction_VersionIncrement(t *testing.T) { assert.Equal(t, "Version 2", *currentResp.SingleFields.ContractTitle) } +func TestGetFieldExtractionByVersion_Success(t *testing.T) { + ctrl, cfg, documentID, _ := setupFieldExtractionsTestController(t) + ctx := t.Context() + + // Create multiple versions with dates for coverage + svc := fieldextraction.New(cfg) + filename := "test.pdf" + + for i := 1; i <= 3; i++ { + contractTitle := fmt.Sprintf("Contract Version %d", i) + clientName := fmt.Sprintf("Client %d", i) + // Include dates to test date conversion coverage + effectiveDate := pgtype.Date{Time: time.Date(2024, 1, i, 0, 0, 0, 0, time.UTC), Valid: true} + terminationDate := pgtype.Date{Time: time.Date(2024, 12, i, 0, 0, 0, 0, time.UTC), Valid: true} + _, err := svc.CreateFieldExtraction(ctx, &fieldextraction.CreateFieldExtractionInput{ + DocumentID: documentID, + SingleFields: &repository.AddFieldExtractionParams{ + Documentid: documentID, + Filename: &filename, + Contracttitle: &contractTitle, + Clientname: &clientName, + Aaretederivedeffectivedt: effectiveDate, + Aaretederivedterminationdt: terminationDate, + Createdby: fmt.Sprintf("user%d@example.com", i), + }, + ArrayFields: []*repository.AddFieldExtractionArrayFieldParams{ + { + Exhibittitle: ptr(fmt.Sprintf("Exhibit V%d", i)), + }, + }, + }) + require.NoError(t, err) + } + + // Test retrieving version 2 + e := echo.New() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/field-extractions/version?documentId=%s&version=2", documentID), nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + params := queryapi.GetFieldExtractionByVersionParams{ + DocumentId: queryapi.DocumentID(documentID), + Version: 2, + } + + err := ctrl.GetFieldExtractionByVersion(c, params) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + // Parse response + var resp queryapi.FieldExtractionResponse + err = json.Unmarshal(rec.Body.Bytes(), &resp) + require.NoError(t, err) + + // Verify we got version 2 + assert.Equal(t, documentID, uuid.UUID(resp.DocumentId)) + assert.Equal(t, int32(2), resp.Version) + assert.Equal(t, "user2@example.com", string(resp.CreatedBy)) + assert.Equal(t, "Contract Version 2", *resp.SingleFields.ContractTitle) + assert.Equal(t, "Client 2", *resp.SingleFields.ClientName) + // Verify dates are returned correctly + require.NotNil(t, resp.SingleFields.AareteDerivedEffectiveDt, "Expected effective date in response") + require.NotNil(t, resp.SingleFields.AareteDerivedTerminationDt, "Expected termination date in response") + assert.Equal(t, 2024, resp.SingleFields.AareteDerivedEffectiveDt.Year()) + assert.Equal(t, time.January, resp.SingleFields.AareteDerivedEffectiveDt.Month()) + assert.Equal(t, 2, resp.SingleFields.AareteDerivedEffectiveDt.Day()) + require.Len(t, resp.ArrayFields, 1) + assert.Equal(t, "Exhibit V2", *resp.ArrayFields[0].ExhibitTitle) + + // Also test retrieving version 1 + req = httptest.NewRequest(http.MethodGet, fmt.Sprintf("/field-extractions/version?documentId=%s&version=1", documentID), nil) + rec = httptest.NewRecorder() + c = e.NewContext(req, rec) + + params.Version = 1 + err = ctrl.GetFieldExtractionByVersion(c, params) + require.NoError(t, err) + assert.Equal(t, http.StatusOK, rec.Code) + + err = json.Unmarshal(rec.Body.Bytes(), &resp) + require.NoError(t, err) + + assert.Equal(t, int32(1), resp.Version) + assert.Equal(t, "Contract Version 1", *resp.SingleFields.ContractTitle) + assert.Equal(t, "Exhibit V1", *resp.ArrayFields[0].ExhibitTitle) +} + +func TestGetFieldExtractionByVersion_NotFound(t *testing.T) { + ctrl, cfg, documentID, _ := setupFieldExtractionsTestController(t) + ctx := t.Context() + + // Create only one version + svc := fieldextraction.New(cfg) + filename := "test.pdf" + contractTitle := "Test Contract" + _, err := svc.CreateFieldExtraction(ctx, &fieldextraction.CreateFieldExtractionInput{ + DocumentID: documentID, + SingleFields: &repository.AddFieldExtractionParams{ + Documentid: documentID, + Filename: &filename, + Contracttitle: &contractTitle, + Createdby: "user@example.com", + }, + ArrayFields: []*repository.AddFieldExtractionArrayFieldParams{}, + }) + require.NoError(t, err) + + // Try to get version 5 (which doesn't exist) + e := echo.New() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/field-extractions/version?documentId=%s&version=5", documentID), nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + params := queryapi.GetFieldExtractionByVersionParams{ + DocumentId: queryapi.DocumentID(documentID), + Version: 5, + } + + err = ctrl.GetFieldExtractionByVersion(c, params) + require.Error(t, err) + httpErr, ok := err.(*echo.HTTPError) + require.True(t, ok) + assert.Equal(t, http.StatusNotFound, httpErr.Code) +} + +func TestGetFieldExtractionByVersion_InvalidDocumentID(t *testing.T) { + ctrl, _, _, _ := setupFieldExtractionsTestController(t) + + e := echo.New() + req := httptest.NewRequest(http.MethodGet, "/field-extractions/version?documentId=&version=1", nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + params := queryapi.GetFieldExtractionByVersionParams{ + DocumentId: queryapi.DocumentID(uuid.Nil), + Version: 1, + } + + err := ctrl.GetFieldExtractionByVersion(c, params) + require.Error(t, err) + httpErr, ok := err.(*echo.HTTPError) + require.True(t, ok) + assert.Equal(t, http.StatusBadRequest, httpErr.Code) +} + +func TestGetFieldExtractionByVersion_InvalidVersion(t *testing.T) { + ctrl, _, documentID, _ := setupFieldExtractionsTestController(t) + + e := echo.New() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/field-extractions/version?documentId=%s&version=0", documentID), nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + params := queryapi.GetFieldExtractionByVersionParams{ + DocumentId: queryapi.DocumentID(documentID), + Version: 0, // Invalid: version must be >= 1 + } + + err := ctrl.GetFieldExtractionByVersion(c, params) + require.Error(t, err) + httpErr, ok := err.(*echo.HTTPError) + require.True(t, ok) + assert.Equal(t, http.StatusBadRequest, httpErr.Code) +} + +func TestGetFieldExtractionByVersion_NoExtractions(t *testing.T) { + ctrl, _, documentID, _ := setupFieldExtractionsTestController(t) + + // Document exists but has no field extractions + e := echo.New() + req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/field-extractions/version?documentId=%s&version=1", documentID), nil) + rec := httptest.NewRecorder() + c := e.NewContext(req, rec) + + params := queryapi.GetFieldExtractionByVersionParams{ + DocumentId: queryapi.DocumentID(documentID), + Version: 1, + } + + err := ctrl.GetFieldExtractionByVersion(c, params) + require.Error(t, err) + httpErr, ok := err.(*echo.HTTPError) + require.True(t, ok) + assert.Equal(t, http.StatusNotFound, httpErr.Code) +} + // Helper function to create string pointer func ptr(s string) *string { return &s diff --git a/internal/client/list.go b/internal/client/list.go new file mode 100644 index 00000000..7b512c34 --- /dev/null +++ b/internal/client/list.go @@ -0,0 +1,20 @@ +package client + +import ( + "context" +) + +// List returns all clients ordered by name. +func (s *Service) List(ctx context.Context) ([]*Client, error) { + clients, err := s.cfg.GetDBQueries().ListClients(ctx) + if err != nil { + return nil, err + } + + result := make([]*Client, len(clients)) + for i, c := range clients { + result[i] = parseFullClient(c) + } + + return result, nil +} diff --git a/internal/database/migrations/00000000000117_add_version_unique_constraint.down.sql b/internal/database/migrations/00000000000117_add_version_unique_constraint.down.sql new file mode 100644 index 00000000..92488e11 --- /dev/null +++ b/internal/database/migrations/00000000000117_add_version_unique_constraint.down.sql @@ -0,0 +1,10 @@ +-- Remove the unique constraint and documentId column + +DROP INDEX IF EXISTS idx_documentfieldextractionversions_documentid; +DROP INDEX IF EXISTS idx_unique_version_per_document; + +ALTER TABLE documentFieldExtractionVersions +DROP CONSTRAINT IF EXISTS fk_documentfieldextractionversions_documentid; + +ALTER TABLE documentFieldExtractionVersions +DROP COLUMN IF EXISTS documentId; diff --git a/internal/database/migrations/00000000000117_add_version_unique_constraint.up.sql b/internal/database/migrations/00000000000117_add_version_unique_constraint.up.sql new file mode 100644 index 00000000..95892fec --- /dev/null +++ b/internal/database/migrations/00000000000117_add_version_unique_constraint.up.sql @@ -0,0 +1,30 @@ +-- Add unique constraint to prevent duplicate versions per document +-- This ensures that concurrent requests cannot create the same version number + +-- First, add documentId column to versions table for easier constraint enforcement +ALTER TABLE documentFieldExtractionVersions +ADD COLUMN documentId uuid; + +-- Populate documentId from the joined extraction record +UPDATE documentFieldExtractionVersions dfev +SET documentId = dfe.documentId +FROM documentFieldExtractions dfe +WHERE dfev.fieldExtractionId = dfe.id; + +-- Make documentId NOT NULL after populating +ALTER TABLE documentFieldExtractionVersions +ALTER COLUMN documentId SET NOT NULL; + +-- Add foreign key constraint (documents table has 'id' as primary key) +ALTER TABLE documentFieldExtractionVersions +ADD CONSTRAINT fk_documentfieldextractionversions_documentid +FOREIGN KEY (documentId) REFERENCES documents(id); + +-- Add unique constraint on documentId + version +-- This prevents duplicate version numbers for the same document +CREATE UNIQUE INDEX idx_unique_version_per_document +ON documentFieldExtractionVersions(documentId, version); + +-- Add index for efficient lookups by documentId +CREATE INDEX idx_documentfieldextractionversions_documentid +ON documentFieldExtractionVersions(documentId); diff --git a/internal/database/queries/client.sql b/internal/database/queries/client.sql index f95287f9..08f7234a 100644 --- a/internal/database/queries/client.sql +++ b/internal/database/queries/client.sql @@ -4,6 +4,10 @@ INSERT INTO clients (clientId, name) VALUES ($1, $2); -- name: GetClient :one SELECT * FROM fullClients WHERE clientId = $1; +-- name: ListClients :many +-- Returns all clients ordered by name +SELECT * FROM fullClients ORDER BY name; + -- name: UpdateClient :exec UPDATE clients SET name = $1 WHERE clientId = $2; diff --git a/internal/database/queries/fieldextractions.sql b/internal/database/queries/fieldextractions.sql index a4318c79..b1151cec 100644 --- a/internal/database/queries/fieldextractions.sql +++ b/internal/database/queries/fieldextractions.sql @@ -158,13 +158,63 @@ INSERT INTO documentFieldExtractionArrayFields ( ); -- name: AddFieldExtractionEntry :exec -INSERT INTO documentFieldExtractionVersions (fieldExtractionId, version, createdBy) -VALUES ($1, $2, $3); +-- Insert a new version entry for a field extraction +-- documentId is required for the unique constraint enforcement +INSERT INTO documentFieldExtractionVersions (fieldExtractionId, documentId, version, createdBy) +VALUES ($1, $2, $3, $4); + +-- name: LockFieldExtractionVersionsForDocument :many +-- Lock all existing version rows for a document to prevent concurrent inserts +-- Must be called within a transaction before GetMaxFieldExtractionVersion +-- Returns the locked rows (which may be empty for first version) +SELECT id, version FROM documentFieldExtractionVersions +WHERE documentId = $1 +FOR UPDATE; + +-- name: GetMaxFieldExtractionVersion :one +-- Get the current max version for a document (call after LockFieldExtractionVersionsForDocument) +-- Returns 0 if no versions exist, otherwise the max version number +-- COALESCE ensures we always get a non-NULL value that can be scanned into int64 +SELECT COALESCE(MAX(version), 0) as max_version +FROM documentFieldExtractionVersions +WHERE documentId = $1; -- name: GetCurrentFieldExtraction :one SELECT * FROM currentFieldExtractions WHERE documentId = $1; +-- name: GetFieldExtractionByVersion :one +-- Retrieves a specific version of field extraction for a document +-- Returns the field extraction record with version metadata +SELECT + dfe.id, + dfe.documentId, + dfe.fileName, + dfe.contractTitle, + dfe.aareteDerivedAmendmentNum, + dfe.clientName, + dfe.payerName, + dfe.payerState, + dfe.providerState, + dfe.filenameTin, + dfe.provGroupTin, + dfe.provGroupNpi, + dfe.provGroupNameFull, + dfe.provOtherTin, + dfe.provOtherNpi, + dfe.provOtherNameFull, + dfe.aareteDerivedEffectiveDt, + dfe.aareteDerivedTerminationDt, + dfe.autoRenewalInd, + dfe.autoRenewalTerm, + dfev.version, + dfev.createdBy, + dfev.createdAt +FROM documentFieldExtractions dfe +JOIN documentFieldExtractionVersions dfev + ON dfev.fieldExtractionId = dfe.id +WHERE dfev.documentId = $1 AND dfev.version = $2; + -- name: GetCurrentFieldExtractionWithArrayCount :one SELECT * FROM currentFieldExtractionsWithArrayCount WHERE documentId = $1; diff --git a/internal/database/repository/client.sql.go b/internal/database/repository/client.sql.go index 5b58b81e..1d0677fe 100644 --- a/internal/database/repository/client.sql.go +++ b/internal/database/repository/client.sql.go @@ -255,6 +255,33 @@ func (q *Queries) IsClientSynced(ctx context.Context, dollar_1 *string) (bool, e return is_synced, err } +const listClients = `-- name: ListClients :many +SELECT clientid, name, cansync FROM fullClients ORDER BY name +` + +// Returns all clients ordered by name +// +// SELECT clientid, name, cansync FROM fullClients ORDER BY name +func (q *Queries) ListClients(ctx context.Context) ([]*Fullclient, error) { + rows, err := q.db.Query(ctx, listClients) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*Fullclient{} + for rows.Next() { + var i Fullclient + if err := rows.Scan(&i.Clientid, &i.Name, &i.Cansync); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const updateClient = `-- name: UpdateClient :exec UPDATE clients SET name = $1 WHERE clientId = $2 ` diff --git a/internal/database/repository/fieldextractions.sql.go b/internal/database/repository/fieldextractions.sql.go index 0c4bc23c..4f337dfa 100644 --- a/internal/database/repository/fieldextractions.sql.go +++ b/internal/database/repository/fieldextractions.sql.go @@ -644,22 +644,29 @@ func (q *Queries) AddFieldExtractionArrayField(ctx context.Context, arg *AddFiel } const addFieldExtractionEntry = `-- name: AddFieldExtractionEntry :exec -INSERT INTO documentFieldExtractionVersions (fieldExtractionId, version, createdBy) -VALUES ($1, $2, $3) +INSERT INTO documentFieldExtractionVersions (fieldExtractionId, documentId, version, createdBy) +VALUES ($1, $2, $3, $4) ` type AddFieldExtractionEntryParams struct { Fieldextractionid uuid.UUID `db:"fieldextractionid"` + Documentid uuid.UUID `db:"documentid"` Version int64 `db:"version"` Createdby string `db:"createdby"` } -// AddFieldExtractionEntry +// Insert a new version entry for a field extraction +// documentId is required for the unique constraint enforcement // -// INSERT INTO documentFieldExtractionVersions (fieldExtractionId, version, createdBy) -// VALUES ($1, $2, $3) +// INSERT INTO documentFieldExtractionVersions (fieldExtractionId, documentId, version, createdBy) +// VALUES ($1, $2, $3, $4) func (q *Queries) AddFieldExtractionEntry(ctx context.Context, arg *AddFieldExtractionEntryParams) error { - _, err := q.db.Exec(ctx, addFieldExtractionEntry, arg.Fieldextractionid, arg.Version, arg.Createdby) + _, err := q.db.Exec(ctx, addFieldExtractionEntry, + arg.Fieldextractionid, + arg.Documentid, + arg.Version, + arg.Createdby, + ) return err } @@ -930,6 +937,130 @@ func (q *Queries) GetFieldExtractionByID(ctx context.Context, id uuid.UUID) (*Do return &i, err } +const getFieldExtractionByVersion = `-- name: GetFieldExtractionByVersion :one +SELECT + dfe.id, + dfe.documentId, + dfe.fileName, + dfe.contractTitle, + dfe.aareteDerivedAmendmentNum, + dfe.clientName, + dfe.payerName, + dfe.payerState, + dfe.providerState, + dfe.filenameTin, + dfe.provGroupTin, + dfe.provGroupNpi, + dfe.provGroupNameFull, + dfe.provOtherTin, + dfe.provOtherNpi, + dfe.provOtherNameFull, + dfe.aareteDerivedEffectiveDt, + dfe.aareteDerivedTerminationDt, + dfe.autoRenewalInd, + dfe.autoRenewalTerm, + dfev.version, + dfev.createdBy, + dfev.createdAt +FROM documentFieldExtractions dfe +JOIN documentFieldExtractionVersions dfev + ON dfev.fieldExtractionId = dfe.id +WHERE dfev.documentId = $1 AND dfev.version = $2 +` + +type GetFieldExtractionByVersionParams struct { + Documentid uuid.UUID `db:"documentid"` + Version int64 `db:"version"` +} + +type GetFieldExtractionByVersionRow struct { + ID uuid.UUID `db:"id"` + Documentid uuid.UUID `db:"documentid"` + Filename *string `db:"filename"` + Contracttitle *string `db:"contracttitle"` + Aaretederivedamendmentnum *int32 `db:"aaretederivedamendmentnum"` + Clientname *string `db:"clientname"` + Payername *string `db:"payername"` + Payerstate *string `db:"payerstate"` + Providerstate *string `db:"providerstate"` + Filenametin *string `db:"filenametin"` + Provgrouptin *string `db:"provgrouptin"` + Provgroupnpi *string `db:"provgroupnpi"` + Provgroupnamefull *string `db:"provgroupnamefull"` + Provothertin *string `db:"provothertin"` + Provothernpi *string `db:"provothernpi"` + Provothernamefull *string `db:"provothernamefull"` + Aaretederivedeffectivedt pgtype.Date `db:"aaretederivedeffectivedt"` + Aaretederivedterminationdt pgtype.Date `db:"aaretederivedterminationdt"` + Autorenewalind *bool `db:"autorenewalind"` + Autorenewalterm *string `db:"autorenewalterm"` + Version int64 `db:"version"` + Createdby string `db:"createdby"` + Createdat pgtype.Timestamp `db:"createdat"` +} + +// Retrieves a specific version of field extraction for a document +// Returns the field extraction record with version metadata +// +// SELECT +// dfe.id, +// dfe.documentId, +// dfe.fileName, +// dfe.contractTitle, +// dfe.aareteDerivedAmendmentNum, +// dfe.clientName, +// dfe.payerName, +// dfe.payerState, +// dfe.providerState, +// dfe.filenameTin, +// dfe.provGroupTin, +// dfe.provGroupNpi, +// dfe.provGroupNameFull, +// dfe.provOtherTin, +// dfe.provOtherNpi, +// dfe.provOtherNameFull, +// dfe.aareteDerivedEffectiveDt, +// dfe.aareteDerivedTerminationDt, +// dfe.autoRenewalInd, +// dfe.autoRenewalTerm, +// dfev.version, +// dfev.createdBy, +// dfev.createdAt +// FROM documentFieldExtractions dfe +// JOIN documentFieldExtractionVersions dfev +// ON dfev.fieldExtractionId = dfe.id +// WHERE dfev.documentId = $1 AND dfev.version = $2 +func (q *Queries) GetFieldExtractionByVersion(ctx context.Context, arg *GetFieldExtractionByVersionParams) (*GetFieldExtractionByVersionRow, error) { + row := q.db.QueryRow(ctx, getFieldExtractionByVersion, arg.Documentid, arg.Version) + var i GetFieldExtractionByVersionRow + err := row.Scan( + &i.ID, + &i.Documentid, + &i.Filename, + &i.Contracttitle, + &i.Aaretederivedamendmentnum, + &i.Clientname, + &i.Payername, + &i.Payerstate, + &i.Providerstate, + &i.Filenametin, + &i.Provgrouptin, + &i.Provgroupnpi, + &i.Provgroupnamefull, + &i.Provothertin, + &i.Provothernpi, + &i.Provothernamefull, + &i.Aaretederivedeffectivedt, + &i.Aaretederivedterminationdt, + &i.Autorenewalind, + &i.Autorenewalterm, + &i.Version, + &i.Createdby, + &i.Createdat, + ) + return &i, err +} + const getFieldExtractionHistory = `-- name: GetFieldExtractionHistory :many SELECT dfe.id, @@ -1045,6 +1176,64 @@ func (q *Queries) GetFieldExtractionsByDocumentID(ctx context.Context, documenti return items, nil } +const getMaxFieldExtractionVersion = `-- name: GetMaxFieldExtractionVersion :one +SELECT COALESCE(MAX(version), 0) as max_version +FROM documentFieldExtractionVersions +WHERE documentId = $1 +` + +// Get the current max version for a document (call after LockFieldExtractionVersionsForDocument) +// Returns 0 if no versions exist, otherwise the max version number +// COALESCE ensures we always get a non-NULL value that can be scanned into int64 +// +// SELECT COALESCE(MAX(version), 0) as max_version +// FROM documentFieldExtractionVersions +// WHERE documentId = $1 +func (q *Queries) GetMaxFieldExtractionVersion(ctx context.Context, documentid uuid.UUID) (*int64, error) { + row := q.db.QueryRow(ctx, getMaxFieldExtractionVersion, documentid) + var max_version *int64 + err := row.Scan(&max_version) + return max_version, err +} + +const lockFieldExtractionVersionsForDocument = `-- name: LockFieldExtractionVersionsForDocument :many +SELECT id, version FROM documentFieldExtractionVersions +WHERE documentId = $1 +FOR UPDATE +` + +type LockFieldExtractionVersionsForDocumentRow struct { + ID uuid.UUID `db:"id"` + Version int64 `db:"version"` +} + +// Lock all existing version rows for a document to prevent concurrent inserts +// Must be called within a transaction before GetMaxFieldExtractionVersion +// Returns the locked rows (which may be empty for first version) +// +// SELECT id, version FROM documentFieldExtractionVersions +// WHERE documentId = $1 +// FOR UPDATE +func (q *Queries) LockFieldExtractionVersionsForDocument(ctx context.Context, documentid uuid.UUID) ([]*LockFieldExtractionVersionsForDocumentRow, error) { + rows, err := q.db.Query(ctx, lockFieldExtractionVersionsForDocument, documentid) + if err != nil { + return nil, err + } + defer rows.Close() + items := []*LockFieldExtractionVersionsForDocumentRow{} + for rows.Next() { + var i LockFieldExtractionVersionsForDocumentRow + if err := rows.Scan(&i.ID, &i.Version); err != nil { + return nil, err + } + items = append(items, &i) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + const validateArrayFieldCount = `-- name: ValidateArrayFieldCount :one SELECT COUNT(*) as arrayFieldCount FROM documentFieldExtractionArrayFields diff --git a/internal/database/repository/models.go b/internal/database/repository/models.go index bed52210..17ac58dc 100644 --- a/internal/database/repository/models.go +++ b/internal/database/repository/models.go @@ -602,6 +602,7 @@ type Documentfieldextractionversion struct { Version int64 `db:"version"` Createdby string `db:"createdby"` Createdat pgtype.Timestamp `db:"createdat"` + Documentid uuid.UUID `db:"documentid"` } type Documentlabel struct { diff --git a/internal/fieldextraction/models.go b/internal/fieldextraction/models.go index 0e45082c..c7402e42 100644 --- a/internal/fieldextraction/models.go +++ b/internal/fieldextraction/models.go @@ -1,6 +1,8 @@ package fieldextraction import ( + "fmt" + "queryorchestration/internal/database/repository" "github.com/google/uuid" @@ -13,8 +15,38 @@ type CreateFieldExtractionInput struct { ArrayFields []*repository.AddFieldExtractionArrayFieldParams // All arrays must be same length N } -// ValidateArrayConsistency checks that all array fields have consistent length -// Returns the array length if valid, error if inconsistent +// MaxArrayFieldCount is the maximum number of array field rows allowed per field extraction. +// This prevents excessively large payloads that could impact performance. +const MaxArrayFieldCount = 10000 + +// ValidateArrayConsistency validates the array fields for a field extraction. +// It checks: +// - No nil entries in the array +// - Array length does not exceed MaxArrayFieldCount +// - Empty arrays are allowed (returns 0, nil) +// +// Note: The service layer assigns arrayIndex values sequentially (0, 1, 2, ...) +// when inserting, so we don't validate incoming arrayIndex values here. +// The database enforces uniqueness and non-negative constraints. +// +// Returns the array length if valid, error if validation fails. func ValidateArrayConsistency(arrayFields []*repository.AddFieldExtractionArrayFieldParams) (int, error) { + // Empty array is valid + if len(arrayFields) == 0 { + return 0, nil + } + + // Check for maximum array size to prevent performance issues + if len(arrayFields) > MaxArrayFieldCount { + return 0, fmt.Errorf("array field count %d exceeds maximum allowed %d", len(arrayFields), MaxArrayFieldCount) + } + + // Check for nil entries which would cause issues during insertion + for i, field := range arrayFields { + if field == nil { + return 0, fmt.Errorf("array field at index %d is nil", i) + } + } + return len(arrayFields), nil } diff --git a/internal/fieldextraction/models_test.go b/internal/fieldextraction/models_test.go new file mode 100644 index 00000000..83177654 --- /dev/null +++ b/internal/fieldextraction/models_test.go @@ -0,0 +1,111 @@ +package fieldextraction + +import ( + "testing" + + "queryorchestration/internal/database/repository" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateArrayConsistency(t *testing.T) { + t.Run("empty array is valid", func(t *testing.T) { + count, err := ValidateArrayConsistency([]*repository.AddFieldExtractionArrayFieldParams{}) + require.NoError(t, err) + assert.Equal(t, 0, count) + }) + + t.Run("nil slice is valid", func(t *testing.T) { + count, err := ValidateArrayConsistency(nil) + require.NoError(t, err) + assert.Equal(t, 0, count) + }) + + t.Run("single element array is valid", func(t *testing.T) { + fields := []*repository.AddFieldExtractionArrayFieldParams{ + {}, + } + count, err := ValidateArrayConsistency(fields) + require.NoError(t, err) + assert.Equal(t, 1, count) + }) + + t.Run("multiple elements array is valid", func(t *testing.T) { + exhibitTitle1 := "Exhibit A" + exhibitTitle2 := "Exhibit B" + exhibitTitle3 := "Exhibit C" + fields := []*repository.AddFieldExtractionArrayFieldParams{ + {Exhibittitle: &exhibitTitle1}, + {Exhibittitle: &exhibitTitle2}, + {Exhibittitle: &exhibitTitle3}, + } + count, err := ValidateArrayConsistency(fields) + require.NoError(t, err) + assert.Equal(t, 3, count) + }) + + t.Run("nil entry in array returns error", func(t *testing.T) { + exhibitTitle := "Exhibit A" + fields := []*repository.AddFieldExtractionArrayFieldParams{ + {Exhibittitle: &exhibitTitle}, + nil, // nil entry + {Exhibittitle: &exhibitTitle}, + } + count, err := ValidateArrayConsistency(fields) + require.Error(t, err) + assert.Equal(t, 0, count) + assert.Contains(t, err.Error(), "array field at index 1 is nil") + }) + + t.Run("nil entry at first position returns error", func(t *testing.T) { + exhibitTitle := "Exhibit A" + fields := []*repository.AddFieldExtractionArrayFieldParams{ + nil, // nil entry at first position + {Exhibittitle: &exhibitTitle}, + } + count, err := ValidateArrayConsistency(fields) + require.Error(t, err) + assert.Equal(t, 0, count) + assert.Contains(t, err.Error(), "array field at index 0 is nil") + }) + + t.Run("nil entry at last position returns error", func(t *testing.T) { + exhibitTitle := "Exhibit A" + fields := []*repository.AddFieldExtractionArrayFieldParams{ + {Exhibittitle: &exhibitTitle}, + {Exhibittitle: &exhibitTitle}, + nil, // nil entry at last position + } + count, err := ValidateArrayConsistency(fields) + require.Error(t, err) + assert.Equal(t, 0, count) + assert.Contains(t, err.Error(), "array field at index 2 is nil") + }) + + t.Run("array at max count is valid", func(t *testing.T) { + fields := make([]*repository.AddFieldExtractionArrayFieldParams, MaxArrayFieldCount) + for i := range fields { + fields[i] = &repository.AddFieldExtractionArrayFieldParams{} + } + count, err := ValidateArrayConsistency(fields) + require.NoError(t, err) + assert.Equal(t, MaxArrayFieldCount, count) + }) + + t.Run("array exceeding max count returns error", func(t *testing.T) { + fields := make([]*repository.AddFieldExtractionArrayFieldParams, MaxArrayFieldCount+1) + for i := range fields { + fields[i] = &repository.AddFieldExtractionArrayFieldParams{} + } + count, err := ValidateArrayConsistency(fields) + require.Error(t, err) + assert.Equal(t, 0, count) + assert.Contains(t, err.Error(), "exceeds maximum allowed") + }) +} + +func TestMaxArrayFieldCount(t *testing.T) { + // Verify the constant is set to a reasonable value + assert.Equal(t, 10000, MaxArrayFieldCount, "MaxArrayFieldCount should be 10000") +} diff --git a/internal/fieldextraction/service.go b/internal/fieldextraction/service.go index 8cf4d244..dd9229cc 100644 --- a/internal/fieldextraction/service.go +++ b/internal/fieldextraction/service.go @@ -67,16 +67,32 @@ func (s *Service) CreateFieldExtraction(ctx context.Context, input *CreateFieldE return nil, fmt.Errorf("failed to create field extraction: %w", err) } - // Get the next version number for this document - history, err := queries.GetFieldExtractionsByDocumentID(ctx, input.DocumentID) + // Lock existing version rows for this document to prevent concurrent version creation + // This ensures that two concurrent requests cannot create the same version number + _, err = queries.LockFieldExtractionVersionsForDocument(ctx, input.DocumentID) if err != nil { - return nil, fmt.Errorf("failed to get field extraction history: %w", err) + return nil, fmt.Errorf("failed to lock version rows: %w", err) } - version := int64(len(history)) - // Create version entry + // Get the max version number for this document (after locking) + // COALESCE in the query ensures we get 0 if no versions exist + maxVersionPtr, err := queries.GetMaxFieldExtractionVersion(ctx, input.DocumentID) + if err != nil { + return nil, fmt.Errorf("failed to get max version: %w", err) + } + // SQLC returns *int64 even though COALESCE guarantees non-NULL + // Dereference safely - if somehow nil, treat as 0 + var maxVersion int64 + if maxVersionPtr != nil { + maxVersion = *maxVersionPtr + } + // Next version is always maxVersion + 1 (1 for first version since maxVersion is 0) + version := maxVersion + 1 + + // Create version entry with documentId for the unique constraint err = queries.AddFieldExtractionEntry(ctx, &repository.AddFieldExtractionEntryParams{ Fieldextractionid: fieldExtraction.ID, + Documentid: input.DocumentID, Version: version, Createdby: input.SingleFields.Createdby, }) @@ -152,3 +168,30 @@ func (s *Service) GetFieldExtractionArrayFields(ctx context.Context, fieldExtrac return arrayFields, nil } + +// GetFieldExtractionByVersion retrieves a specific version of field extraction for a document +// Returns nil if no field extraction exists for the document at that version +// documentID: the UUID of the document +// version: the version number to retrieve (must be >= 1) +func (s *Service) GetFieldExtractionByVersion(ctx context.Context, documentID uuid.UUID, version int64) (*repository.GetFieldExtractionByVersionRow, error) { + if documentID == uuid.Nil { + return nil, fmt.Errorf("documentID cannot be nil") + } + + if version < 1 { + return nil, fmt.Errorf("version must be at least 1") + } + + fieldExtraction, err := s.cfg.GetDBQueries().GetFieldExtractionByVersion(ctx, &repository.GetFieldExtractionByVersionParams{ + Documentid: documentID, + Version: version, + }) + if err != nil { + if err == pgx.ErrNoRows { + return nil, nil + } + return nil, fmt.Errorf("failed to get field extraction by version: %w", err) + } + + return fieldExtraction, nil +} diff --git a/mocks/queryapi/mock_ClientInterface.go b/mocks/queryapi/mock_ClientInterface.go index 4ed98efd..c5d70308 100644 --- a/mocks/queryapi/mock_ClientInterface.go +++ b/mocks/queryapi/mock_ClientInterface.go @@ -1964,6 +1964,80 @@ func (_c *MockClientInterface_GetDocumentsByLabel_Call) RunAndReturn(run func(co return _c } +// GetFieldExtractionByVersion provides a mock function with given fields: ctx, params, reqEditors +func (_m *MockClientInterface) GetFieldExtractionByVersion(ctx context.Context, params *queryapi.GetFieldExtractionByVersionParams, reqEditors ...queryapi.RequestEditorFn) (*http.Response, error) { + _va := make([]interface{}, len(reqEditors)) + for _i := range reqEditors { + _va[_i] = reqEditors[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, params) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetFieldExtractionByVersion") + } + + var r0 *http.Response + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *queryapi.GetFieldExtractionByVersionParams, ...queryapi.RequestEditorFn) (*http.Response, error)); ok { + return rf(ctx, params, reqEditors...) + } + if rf, ok := ret.Get(0).(func(context.Context, *queryapi.GetFieldExtractionByVersionParams, ...queryapi.RequestEditorFn) *http.Response); ok { + r0 = rf(ctx, params, reqEditors...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Response) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *queryapi.GetFieldExtractionByVersionParams, ...queryapi.RequestEditorFn) error); ok { + r1 = rf(ctx, params, reqEditors...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClientInterface_GetFieldExtractionByVersion_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFieldExtractionByVersion' +type MockClientInterface_GetFieldExtractionByVersion_Call struct { + *mock.Call +} + +// GetFieldExtractionByVersion is a helper method to define mock.On call +// - ctx context.Context +// - params *queryapi.GetFieldExtractionByVersionParams +// - reqEditors ...queryapi.RequestEditorFn +func (_e *MockClientInterface_Expecter) GetFieldExtractionByVersion(ctx interface{}, params interface{}, reqEditors ...interface{}) *MockClientInterface_GetFieldExtractionByVersion_Call { + return &MockClientInterface_GetFieldExtractionByVersion_Call{Call: _e.mock.On("GetFieldExtractionByVersion", + append([]interface{}{ctx, params}, reqEditors...)...)} +} + +func (_c *MockClientInterface_GetFieldExtractionByVersion_Call) Run(run func(ctx context.Context, params *queryapi.GetFieldExtractionByVersionParams, reqEditors ...queryapi.RequestEditorFn)) *MockClientInterface_GetFieldExtractionByVersion_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]queryapi.RequestEditorFn, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(queryapi.RequestEditorFn) + } + } + run(args[0].(context.Context), args[1].(*queryapi.GetFieldExtractionByVersionParams), variadicArgs...) + }) + return _c +} + +func (_c *MockClientInterface_GetFieldExtractionByVersion_Call) Return(_a0 *http.Response, _a1 error) *MockClientInterface_GetFieldExtractionByVersion_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClientInterface_GetFieldExtractionByVersion_Call) RunAndReturn(run func(context.Context, *queryapi.GetFieldExtractionByVersionParams, ...queryapi.RequestEditorFn) (*http.Response, error)) *MockClientInterface_GetFieldExtractionByVersion_Call { + _c.Call.Return(run) + return _c +} + // GetFieldExtractionHistory provides a mock function with given fields: ctx, params, reqEditors func (_m *MockClientInterface) GetFieldExtractionHistory(ctx context.Context, params *queryapi.GetFieldExtractionHistoryParams, reqEditors ...queryapi.RequestEditorFn) (*http.Response, error) { _va := make([]interface{}, len(reqEditors)) @@ -2555,6 +2629,79 @@ func (_c *MockClientInterface_ListClientFolders_Call) RunAndReturn(run func(cont return _c } +// ListClients provides a mock function with given fields: ctx, reqEditors +func (_m *MockClientInterface) ListClients(ctx context.Context, reqEditors ...queryapi.RequestEditorFn) (*http.Response, error) { + _va := make([]interface{}, len(reqEditors)) + for _i := range reqEditors { + _va[_i] = reqEditors[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for ListClients") + } + + var r0 *http.Response + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ...queryapi.RequestEditorFn) (*http.Response, error)); ok { + return rf(ctx, reqEditors...) + } + if rf, ok := ret.Get(0).(func(context.Context, ...queryapi.RequestEditorFn) *http.Response); ok { + r0 = rf(ctx, reqEditors...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*http.Response) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ...queryapi.RequestEditorFn) error); ok { + r1 = rf(ctx, reqEditors...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClientInterface_ListClients_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListClients' +type MockClientInterface_ListClients_Call struct { + *mock.Call +} + +// ListClients is a helper method to define mock.On call +// - ctx context.Context +// - reqEditors ...queryapi.RequestEditorFn +func (_e *MockClientInterface_Expecter) ListClients(ctx interface{}, reqEditors ...interface{}) *MockClientInterface_ListClients_Call { + return &MockClientInterface_ListClients_Call{Call: _e.mock.On("ListClients", + append([]interface{}{ctx}, reqEditors...)...)} +} + +func (_c *MockClientInterface_ListClients_Call) Run(run func(ctx context.Context, reqEditors ...queryapi.RequestEditorFn)) *MockClientInterface_ListClients_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]queryapi.RequestEditorFn, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(queryapi.RequestEditorFn) + } + } + run(args[0].(context.Context), variadicArgs...) + }) + return _c +} + +func (_c *MockClientInterface_ListClients_Call) Return(_a0 *http.Response, _a1 error) *MockClientInterface_ListClients_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClientInterface_ListClients_Call) RunAndReturn(run func(context.Context, ...queryapi.RequestEditorFn) (*http.Response, error)) *MockClientInterface_ListClients_Call { + _c.Call.Return(run) + return _c +} + // ListDocumentBatches provides a mock function with given fields: ctx, id, params, reqEditors func (_m *MockClientInterface) ListDocumentBatches(ctx context.Context, id queryapi.ClientID, params *queryapi.ListDocumentBatchesParams, reqEditors ...queryapi.RequestEditorFn) (*http.Response, error) { _va := make([]interface{}, len(reqEditors)) diff --git a/mocks/queryapi/mock_ClientWithResponsesInterface.go b/mocks/queryapi/mock_ClientWithResponsesInterface.go index 48c98064..f24daa99 100644 --- a/mocks/queryapi/mock_ClientWithResponsesInterface.go +++ b/mocks/queryapi/mock_ClientWithResponsesInterface.go @@ -1962,6 +1962,80 @@ func (_c *MockClientWithResponsesInterface_GetDocumentsByLabelWithResponse_Call) return _c } +// GetFieldExtractionByVersionWithResponse provides a mock function with given fields: ctx, params, reqEditors +func (_m *MockClientWithResponsesInterface) GetFieldExtractionByVersionWithResponse(ctx context.Context, params *queryapi.GetFieldExtractionByVersionParams, reqEditors ...queryapi.RequestEditorFn) (*queryapi.GetFieldExtractionByVersionResponse, error) { + _va := make([]interface{}, len(reqEditors)) + for _i := range reqEditors { + _va[_i] = reqEditors[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, params) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for GetFieldExtractionByVersionWithResponse") + } + + var r0 *queryapi.GetFieldExtractionByVersionResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *queryapi.GetFieldExtractionByVersionParams, ...queryapi.RequestEditorFn) (*queryapi.GetFieldExtractionByVersionResponse, error)); ok { + return rf(ctx, params, reqEditors...) + } + if rf, ok := ret.Get(0).(func(context.Context, *queryapi.GetFieldExtractionByVersionParams, ...queryapi.RequestEditorFn) *queryapi.GetFieldExtractionByVersionResponse); ok { + r0 = rf(ctx, params, reqEditors...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*queryapi.GetFieldExtractionByVersionResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *queryapi.GetFieldExtractionByVersionParams, ...queryapi.RequestEditorFn) error); ok { + r1 = rf(ctx, params, reqEditors...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClientWithResponsesInterface_GetFieldExtractionByVersionWithResponse_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetFieldExtractionByVersionWithResponse' +type MockClientWithResponsesInterface_GetFieldExtractionByVersionWithResponse_Call struct { + *mock.Call +} + +// GetFieldExtractionByVersionWithResponse is a helper method to define mock.On call +// - ctx context.Context +// - params *queryapi.GetFieldExtractionByVersionParams +// - reqEditors ...queryapi.RequestEditorFn +func (_e *MockClientWithResponsesInterface_Expecter) GetFieldExtractionByVersionWithResponse(ctx interface{}, params interface{}, reqEditors ...interface{}) *MockClientWithResponsesInterface_GetFieldExtractionByVersionWithResponse_Call { + return &MockClientWithResponsesInterface_GetFieldExtractionByVersionWithResponse_Call{Call: _e.mock.On("GetFieldExtractionByVersionWithResponse", + append([]interface{}{ctx, params}, reqEditors...)...)} +} + +func (_c *MockClientWithResponsesInterface_GetFieldExtractionByVersionWithResponse_Call) Run(run func(ctx context.Context, params *queryapi.GetFieldExtractionByVersionParams, reqEditors ...queryapi.RequestEditorFn)) *MockClientWithResponsesInterface_GetFieldExtractionByVersionWithResponse_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]queryapi.RequestEditorFn, len(args)-2) + for i, a := range args[2:] { + if a != nil { + variadicArgs[i] = a.(queryapi.RequestEditorFn) + } + } + run(args[0].(context.Context), args[1].(*queryapi.GetFieldExtractionByVersionParams), variadicArgs...) + }) + return _c +} + +func (_c *MockClientWithResponsesInterface_GetFieldExtractionByVersionWithResponse_Call) Return(_a0 *queryapi.GetFieldExtractionByVersionResponse, _a1 error) *MockClientWithResponsesInterface_GetFieldExtractionByVersionWithResponse_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClientWithResponsesInterface_GetFieldExtractionByVersionWithResponse_Call) RunAndReturn(run func(context.Context, *queryapi.GetFieldExtractionByVersionParams, ...queryapi.RequestEditorFn) (*queryapi.GetFieldExtractionByVersionResponse, error)) *MockClientWithResponsesInterface_GetFieldExtractionByVersionWithResponse_Call { + _c.Call.Return(run) + return _c +} + // GetFieldExtractionHistoryWithResponse provides a mock function with given fields: ctx, params, reqEditors func (_m *MockClientWithResponsesInterface) GetFieldExtractionHistoryWithResponse(ctx context.Context, params *queryapi.GetFieldExtractionHistoryParams, reqEditors ...queryapi.RequestEditorFn) (*queryapi.GetFieldExtractionHistoryResponse, error) { _va := make([]interface{}, len(reqEditors)) @@ -2553,6 +2627,79 @@ func (_c *MockClientWithResponsesInterface_ListClientFoldersWithResponse_Call) R return _c } +// ListClientsWithResponse provides a mock function with given fields: ctx, reqEditors +func (_m *MockClientWithResponsesInterface) ListClientsWithResponse(ctx context.Context, reqEditors ...queryapi.RequestEditorFn) (*queryapi.ListClientsResponse, error) { + _va := make([]interface{}, len(reqEditors)) + for _i := range reqEditors { + _va[_i] = reqEditors[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + if len(ret) == 0 { + panic("no return value specified for ListClientsWithResponse") + } + + var r0 *queryapi.ListClientsResponse + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, ...queryapi.RequestEditorFn) (*queryapi.ListClientsResponse, error)); ok { + return rf(ctx, reqEditors...) + } + if rf, ok := ret.Get(0).(func(context.Context, ...queryapi.RequestEditorFn) *queryapi.ListClientsResponse); ok { + r0 = rf(ctx, reqEditors...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*queryapi.ListClientsResponse) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, ...queryapi.RequestEditorFn) error); ok { + r1 = rf(ctx, reqEditors...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockClientWithResponsesInterface_ListClientsWithResponse_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListClientsWithResponse' +type MockClientWithResponsesInterface_ListClientsWithResponse_Call struct { + *mock.Call +} + +// ListClientsWithResponse is a helper method to define mock.On call +// - ctx context.Context +// - reqEditors ...queryapi.RequestEditorFn +func (_e *MockClientWithResponsesInterface_Expecter) ListClientsWithResponse(ctx interface{}, reqEditors ...interface{}) *MockClientWithResponsesInterface_ListClientsWithResponse_Call { + return &MockClientWithResponsesInterface_ListClientsWithResponse_Call{Call: _e.mock.On("ListClientsWithResponse", + append([]interface{}{ctx}, reqEditors...)...)} +} + +func (_c *MockClientWithResponsesInterface_ListClientsWithResponse_Call) Run(run func(ctx context.Context, reqEditors ...queryapi.RequestEditorFn)) *MockClientWithResponsesInterface_ListClientsWithResponse_Call { + _c.Call.Run(func(args mock.Arguments) { + variadicArgs := make([]queryapi.RequestEditorFn, len(args)-1) + for i, a := range args[1:] { + if a != nil { + variadicArgs[i] = a.(queryapi.RequestEditorFn) + } + } + run(args[0].(context.Context), variadicArgs...) + }) + return _c +} + +func (_c *MockClientWithResponsesInterface_ListClientsWithResponse_Call) Return(_a0 *queryapi.ListClientsResponse, _a1 error) *MockClientWithResponsesInterface_ListClientsWithResponse_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockClientWithResponsesInterface_ListClientsWithResponse_Call) RunAndReturn(run func(context.Context, ...queryapi.RequestEditorFn) (*queryapi.ListClientsResponse, error)) *MockClientWithResponsesInterface_ListClientsWithResponse_Call { + _c.Call.Return(run) + return _c +} + // ListDocumentBatchesWithResponse provides a mock function with given fields: ctx, id, params, reqEditors func (_m *MockClientWithResponsesInterface) ListDocumentBatchesWithResponse(ctx context.Context, id queryapi.ClientID, params *queryapi.ListDocumentBatchesParams, reqEditors ...queryapi.RequestEditorFn) (*queryapi.ListDocumentBatchesResponse, error) { _va := make([]interface{}, len(reqEditors)) diff --git a/pkg/queryAPI/api.gen.go b/pkg/queryAPI/api.gen.go index 061e8e74..42d15c7c 100644 --- a/pkg/queryAPI/api.gen.go +++ b/pkg/queryAPI/api.gen.go @@ -246,6 +246,7 @@ type ArrayFieldItem struct { AareteDerivedClaimTypeCd *string `json:"aareteDerivedClaimTypeCd,omitempty"` AareteDerivedFeeSchedule *string `json:"aareteDerivedFeeSchedule,omitempty"` AareteDerivedFeeScheduleVersion *string `json:"aareteDerivedFeeScheduleVersion,omitempty"` + AareteDerivedGrouperVersion *string `json:"aareteDerivedGrouperVersion,omitempty"` AareteDerivedLob *string `json:"aareteDerivedLob,omitempty"` AareteDerivedNetwork *string `json:"aareteDerivedNetwork,omitempty"` AareteDerivedProduct *string `json:"aareteDerivedProduct,omitempty"` @@ -270,19 +271,48 @@ type ArrayFieldItem struct { DefaultInd *bool `json:"defaultInd,omitempty"` DiagCd *string `json:"diagCd,omitempty"` DiagCdDesc *string `json:"diagCdDesc,omitempty"` + DshFeeRate *float64 `json:"dshFeeRate,omitempty"` + DshInd *bool `json:"dshInd,omitempty"` + DshPctRate *float64 `json:"dshPctRate,omitempty"` ExhibitPage *string `json:"exhibitPage,omitempty"` ExhibitTitle *string `json:"exhibitTitle,omitempty"` + FacilityAdjustmentTerm *string `json:"facilityAdjustmentTerm,omitempty"` + GmeFeeRate *float64 `json:"gmeFeeRate,omitempty"` + GmeInd *bool `json:"gmeInd,omitempty"` + GmePctRate *float64 `json:"gmePctRate,omitempty"` GreaterOfInd *bool `json:"greaterOfInd,omitempty"` + GrouperAlternativeLevelOfCare *string `json:"grouperAlternativeLevelOfCare,omitempty"` GrouperBaseRate *float64 `json:"grouperBaseRate,omitempty"` GrouperCd *string `json:"grouperCd,omitempty"` GrouperCdDesc *string `json:"grouperCdDesc,omitempty"` + GrouperHacInd *bool `json:"grouperHacInd,omitempty"` GrouperPctRate *float64 `json:"grouperPctRate,omitempty"` + GrouperReadmissionsInd *bool `json:"grouperReadmissionsInd,omitempty"` + GrouperRiskOfMortalitySubclass *string `json:"grouperRiskOfMortalitySubclass,omitempty"` + GrouperSeverity *string `json:"grouperSeverity,omitempty"` + GrouperSeverityInd *bool `json:"grouperSeverityInd,omitempty"` + GrouperTransferInd *bool `json:"grouperTransferInd,omitempty"` GrouperType *string `json:"grouperType,omitempty"` + ImeFeeRate *float64 `json:"imeFeeRate,omitempty"` + ImeInd *bool `json:"imeInd,omitempty"` + ImePctRate *float64 `json:"imePctRate,omitempty"` LesserOfInd *bool `json:"lesserOfInd,omitempty"` LobProductRelationship *string `json:"lobProductRelationship,omitempty"` LobProgramRelationship *string `json:"lobProgramRelationship,omitempty"` NdcCd *string `json:"ndcCd,omitempty"` NdcCdDesc *string `json:"ndcCdDesc,omitempty"` + NtapFeeRate *float64 `json:"ntapFeeRate,omitempty"` + NtapInd *bool `json:"ntapInd,omitempty"` + NtapPctRate *float64 `json:"ntapPctRate,omitempty"` + OutlierExclusionCd *string `json:"outlierExclusionCd,omitempty"` + OutlierExclusionCdDesc *string `json:"outlierExclusionCdDesc,omitempty"` + OutlierFirstDollarInd *bool `json:"outlierFirstDollarInd,omitempty"` + OutlierFixedLossNbrDaysThreshold *float64 `json:"outlierFixedLossNbrDaysThreshold,omitempty"` + OutlierFixedLossThreshold *float64 `json:"outlierFixedLossThreshold,omitempty"` + OutlierMaximum *float64 `json:"outlierMaximum,omitempty"` + OutlierMaximumFrequency *float64 `json:"outlierMaximumFrequency,omitempty"` + OutlierPctRate *float64 `json:"outlierPctRate,omitempty"` + OutlierTerm *string `json:"outlierTerm,omitempty"` PatientAgeMax *string `json:"patientAgeMax,omitempty"` PatientAgeMin *string `json:"patientAgeMin,omitempty"` PlaceOfServiceCd *string `json:"placeOfServiceCd,omitempty"` @@ -291,6 +321,11 @@ type ArrayFieldItem struct { ProvSpecialtyCdDesc *string `json:"provSpecialtyCdDesc,omitempty"` ProvTaxonomyCd *string `json:"provTaxonomyCd,omitempty"` ProvTaxonomyCdDesc *string `json:"provTaxonomyCdDesc,omitempty"` + RangeNbrDays *string `json:"rangeNbrDays,omitempty"` + RateEscalatorDesc *string `json:"rateEscalatorDesc,omitempty"` + RateEscalatorInd *bool `json:"rateEscalatorInd,omitempty"` + RateEscalatorMaxRateIncPct *float64 `json:"rateEscalatorMaxRateIncPct,omitempty"` + RateEscalatorRateChangeTimeline *float64 `json:"rateEscalatorRateChangeTimeline,omitempty"` ReimbConversionFactor *float64 `json:"reimbConversionFactor,omitempty"` ReimbEffectiveDt *openapi_types.Date `json:"reimbEffectiveDt,omitempty"` ReimbFeeRate *float64 `json:"reimbFeeRate,omitempty"` @@ -303,8 +338,21 @@ type ArrayFieldItem struct { RevenueCd *string `json:"revenueCd,omitempty"` RevenueCdDesc *string `json:"revenueCdDesc,omitempty"` ServiceTerm *string `json:"serviceTerm,omitempty"` + StopLossDailyMaxRate *float64 `json:"stopLossDailyMaxRate,omitempty"` + StopLossExclusionCd *string `json:"stopLossExclusionCd,omitempty"` + StopLossExclusionDesc *string `json:"stopLossExclusionDesc,omitempty"` + StopLossFirstDollarInd *bool `json:"stopLossFirstDollarInd,omitempty"` + StopLossFixedLossThreshold *float64 `json:"stopLossFixedLossThreshold,omitempty"` + StopLossMaximum *float64 `json:"stopLossMaximum,omitempty"` + StopLossMaximumFrequency *float64 `json:"stopLossMaximumFrequency,omitempty"` + StopLossPctRateOnExcessCharges *float64 `json:"stopLossPctRateOnExcessCharges,omitempty"` + StopLossRangeNbrDays *float64 `json:"stopLossRangeNbrDays,omitempty"` + StopLossTerm *string `json:"stopLossTerm,omitempty"` TriggerBaseThreshold *float64 `json:"triggerBaseThreshold,omitempty"` TriggerCapThresholdAmt *float64 `json:"triggerCapThresholdAmt,omitempty"` + UcFeeRate *float64 `json:"ucFeeRate,omitempty"` + UcInd *bool `json:"ucInd,omitempty"` + UcPctRate *float64 `json:"ucPctRate,omitempty"` UnitOfMeasure *string `json:"unitOfMeasure,omitempty"` } @@ -430,6 +478,15 @@ type ClientIDBody struct { Id ClientID `json:"id"` } +// ClientListResponse Response containing a list of all clients. +type ClientListResponse struct { + // Clients List of clients in the system. + Clients []DocClient `json:"clients"` + + // TotalCount Total number of clients. + TotalCount int32 `json:"totalCount"` +} + // ClientName The client name type ClientName = string @@ -993,6 +1050,15 @@ type GetFieldExtractionHistoryParams struct { DocumentId openapi_types.UUID `form:"documentId" json:"documentId"` } +// GetFieldExtractionByVersionParams defines parameters for GetFieldExtractionByVersion. +type GetFieldExtractionByVersionParams struct { + // DocumentId The document ID + DocumentId openapi_types.UUID `form:"documentId" json:"documentId"` + + // Version The version number to retrieve + Version int64 `form:"version" json:"version"` +} + // GetDocumentsByLabelParams defines parameters for GetDocumentsByLabel. type GetDocumentsByLabelParams struct { // ClientId The client ID to filter documents @@ -1204,6 +1270,9 @@ type ClientInterface interface { // GetStatusByClientId request GetStatusByClientId(ctx context.Context, id ClientID, reqEditors ...RequestEditorFn) (*http.Response, error) + // ListClients request + ListClients(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetDocument request GetDocument(ctx context.Context, id DocumentID, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1229,6 +1298,9 @@ type ClientInterface interface { // GetFieldExtractionHistory request GetFieldExtractionHistory(ctx context.Context, params *GetFieldExtractionHistoryParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // GetFieldExtractionByVersion request + GetFieldExtractionByVersion(ctx context.Context, params *GetFieldExtractionByVersionParams, reqEditors ...RequestEditorFn) (*http.Response, error) + // CreateFolderWithBody request with any body CreateFolderWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1618,6 +1690,18 @@ func (c *Client) GetStatusByClientId(ctx context.Context, id ClientID, reqEditor return c.Client.Do(req) } +func (c *Client) ListClients(ctx context.Context, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewListClientsRequest(c.Server) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) GetDocument(ctx context.Context, id DocumentID, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewGetDocumentRequest(c.Server, id) if err != nil { @@ -1726,6 +1810,18 @@ func (c *Client) GetFieldExtractionHistory(ctx context.Context, params *GetField return c.Client.Do(req) } +func (c *Client) GetFieldExtractionByVersion(ctx context.Context, params *GetFieldExtractionByVersionParams, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewGetFieldExtractionByVersionRequest(c.Server, params) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) CreateFolderWithBody(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewCreateFolderRequestWithBody(c.Server, contentType, body) if err != nil { @@ -2935,6 +3031,33 @@ func NewGetStatusByClientIdRequest(server string, id ClientID) (*http.Request, e return req, nil } +// NewListClientsRequest generates requests for ListClients +func NewListClientsRequest(server string) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/clients") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewGetDocumentRequest generates requests for GetDocument func NewGetDocumentRequest(server string, id DocumentID) (*http.Request, error) { var err error @@ -3214,6 +3337,63 @@ func NewGetFieldExtractionHistoryRequest(server string, params *GetFieldExtracti return req, nil } +// NewGetFieldExtractionByVersionRequest generates requests for GetFieldExtractionByVersion +func NewGetFieldExtractionByVersionRequest(server string, params *GetFieldExtractionByVersionParams) (*http.Request, error) { + var err error + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/field-extractions/version") + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + if params != nil { + queryValues := queryURL.Query() + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "documentId", runtime.ParamLocationQuery, params.DocumentId); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + if queryFrag, err := runtime.StyleParamWithLocation("form", true, "version", runtime.ParamLocationQuery, params.Version); err != nil { + return nil, err + } else if parsed, err := url.ParseQuery(queryFrag); err != nil { + return nil, err + } else { + for k, v := range parsed { + for _, v2 := range v { + queryValues.Add(k, v2) + } + } + } + + queryURL.RawQuery = queryValues.Encode() + } + + req, err := http.NewRequest("GET", queryURL.String(), nil) + if err != nil { + return nil, err + } + + return req, nil +} + // NewCreateFolderRequest calls the generic CreateFolder builder with application/json body func NewCreateFolderRequest(server string, body CreateFolderJSONRequestBody) (*http.Request, error) { var bodyReader io.Reader @@ -3875,6 +4055,9 @@ type ClientWithResponsesInterface interface { // GetStatusByClientIdWithResponse request GetStatusByClientIdWithResponse(ctx context.Context, id ClientID, reqEditors ...RequestEditorFn) (*GetStatusByClientIdResponse, error) + // ListClientsWithResponse request + ListClientsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListClientsResponse, error) + // GetDocumentWithResponse request GetDocumentWithResponse(ctx context.Context, id DocumentID, reqEditors ...RequestEditorFn) (*GetDocumentResponse, error) @@ -3900,6 +4083,9 @@ type ClientWithResponsesInterface interface { // GetFieldExtractionHistoryWithResponse request GetFieldExtractionHistoryWithResponse(ctx context.Context, params *GetFieldExtractionHistoryParams, reqEditors ...RequestEditorFn) (*GetFieldExtractionHistoryResponse, error) + // GetFieldExtractionByVersionWithResponse request + GetFieldExtractionByVersionWithResponse(ctx context.Context, params *GetFieldExtractionByVersionParams, reqEditors ...RequestEditorFn) (*GetFieldExtractionByVersionResponse, error) + // CreateFolderWithBodyWithResponse request with any body CreateFolderWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateFolderResponse, error) @@ -4540,6 +4726,32 @@ func (r GetStatusByClientIdResponse) StatusCode() int { return 0 } +type ListClientsResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ClientListResponse + JSON400 *InvalidRequest + JSON401 *Unauthorized + JSON429 *TooManyRequests + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r ListClientsResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ListClientsResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type GetDocumentResponse struct { Body []byte HTTPResponse *http.Response @@ -4731,6 +4943,33 @@ func (r GetFieldExtractionHistoryResponse) StatusCode() int { return 0 } +type GetFieldExtractionByVersionResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *FieldExtractionResponse + JSON400 *InvalidRequest + JSON401 *Unauthorized + JSON404 *NotFound + JSON429 *TooManyRequests + JSON500 *InternalError +} + +// Status returns HTTPResponse.Status +func (r GetFieldExtractionByVersionResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r GetFieldExtractionByVersionResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + type CreateFolderResponse struct { Body []byte HTTPResponse *http.Response @@ -5343,6 +5582,15 @@ func (c *ClientWithResponses) GetStatusByClientIdWithResponse(ctx context.Contex return ParseGetStatusByClientIdResponse(rsp) } +// ListClientsWithResponse request returning *ListClientsResponse +func (c *ClientWithResponses) ListClientsWithResponse(ctx context.Context, reqEditors ...RequestEditorFn) (*ListClientsResponse, error) { + rsp, err := c.ListClients(ctx, reqEditors...) + if err != nil { + return nil, err + } + return ParseListClientsResponse(rsp) +} + // GetDocumentWithResponse request returning *GetDocumentResponse func (c *ClientWithResponses) GetDocumentWithResponse(ctx context.Context, id DocumentID, reqEditors ...RequestEditorFn) (*GetDocumentResponse, error) { rsp, err := c.GetDocument(ctx, id, reqEditors...) @@ -5422,6 +5670,15 @@ func (c *ClientWithResponses) GetFieldExtractionHistoryWithResponse(ctx context. return ParseGetFieldExtractionHistoryResponse(rsp) } +// GetFieldExtractionByVersionWithResponse request returning *GetFieldExtractionByVersionResponse +func (c *ClientWithResponses) GetFieldExtractionByVersionWithResponse(ctx context.Context, params *GetFieldExtractionByVersionParams, reqEditors ...RequestEditorFn) (*GetFieldExtractionByVersionResponse, error) { + rsp, err := c.GetFieldExtractionByVersion(ctx, params, reqEditors...) + if err != nil { + return nil, err + } + return ParseGetFieldExtractionByVersionResponse(rsp) +} + // CreateFolderWithBodyWithResponse request with arbitrary body returning *CreateFolderResponse func (c *ClientWithResponses) CreateFolderWithBodyWithResponse(ctx context.Context, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*CreateFolderResponse, error) { rsp, err := c.CreateFolderWithBody(ctx, contentType, body, reqEditors...) @@ -6881,6 +7138,60 @@ func ParseGetStatusByClientIdResponse(rsp *http.Response) (*GetStatusByClientIdR return response, nil } +// ParseListClientsResponse parses an HTTP response from a ListClientsWithResponse call +func ParseListClientsResponse(rsp *http.Response) (*ListClientsResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &ListClientsResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ClientListResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest InvalidRequest + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Unauthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429: + var dest TooManyRequests + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON429 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseGetDocumentResponse parses an HTTP response from a GetDocumentWithResponse call func ParseGetDocumentResponse(rsp *http.Response) (*GetDocumentResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) @@ -7298,6 +7609,67 @@ func ParseGetFieldExtractionHistoryResponse(rsp *http.Response) (*GetFieldExtrac return response, nil } +// ParseGetFieldExtractionByVersionResponse parses an HTTP response from a GetFieldExtractionByVersionWithResponse call +func ParseGetFieldExtractionByVersionResponse(rsp *http.Response) (*GetFieldExtractionByVersionResponse, error) { + bodyBytes, err := io.ReadAll(rsp.Body) + defer func() { _ = rsp.Body.Close() }() + if err != nil { + return nil, err + } + + response := &GetFieldExtractionByVersionResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest FieldExtractionResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 400: + var dest InvalidRequest + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON400 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 401: + var dest Unauthorized + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON401 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 404: + var dest NotFound + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON404 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 429: + var dest TooManyRequests + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON429 = &dest + + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 500: + var dest InternalError + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON500 = &dest + + } + + return response, nil +} + // ParseCreateFolderResponse parses an HTTP response from a CreateFolderWithResponse call func ParseCreateFolderResponse(rsp *http.Response) (*CreateFolderResponse, error) { bodyBytes, err := io.ReadAll(rsp.Body) diff --git a/serviceAPIs/queryAPI.yaml b/serviceAPIs/queryAPI.yaml index d837e511..de6e2260 100644 --- a/serviceAPIs/queryAPI.yaml +++ b/serviceAPIs/queryAPI.yaml @@ -73,6 +73,34 @@ paths: "500": $ref: "#/components/responses/InternalError" + /clients: + get: + operationId: listClients + tags: + - ClientService + summary: List all clients + description: Returns a list of all clients in the system. + security: + - jwtAuth: [] + responses: + "200": + description: List of clients retrieved successfully. + headers: + RateLimit: + $ref: "#/components/headers/RateLimit" + content: + application/json: + schema: + $ref: "#/components/schemas/ClientListResponse" + "400": + $ref: "#/components/responses/InvalidRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + /client/{id}: parameters: - $ref: "#/components/parameters/ClientID" @@ -1108,6 +1136,54 @@ paths: "500": $ref: "#/components/responses/InternalError" + /field-extractions/version: + get: + operationId: getFieldExtractionByVersion + tags: + - FieldExtractionService + summary: Get field extraction by version + description: Retrieves a specific version of field extraction for a document. + security: + - jwtAuth: [] + parameters: + - name: documentId + in: query + required: true + description: The document ID + schema: + type: string + format: uuid + maxLength: 50 + - name: version + in: query + required: true + description: The version number to retrieve + schema: + type: integer + format: int64 + minimum: 1 + maximum: 9223372036854775807 + responses: + "200": + description: Field extraction for the specified version. + headers: + RateLimit: + $ref: "#/components/headers/RateLimit" + content: + application/json: + schema: + $ref: "#/components/schemas/FieldExtractionResponse" + "400": + $ref: "#/components/responses/InvalidRequest" + "401": + $ref: "#/components/responses/Unauthorized" + "404": + $ref: "#/components/responses/NotFound" + "429": + $ref: "#/components/responses/TooManyRequests" + "500": + $ref: "#/components/responses/InternalError" + /field-extractions/history: get: operationId: getFieldExtractionHistory @@ -2248,6 +2324,26 @@ components: required: - id + ClientListResponse: + description: Response containing a list of all clients. + type: object + properties: + clients: + type: array + description: List of clients in the system. + maxItems: 10000 + items: + $ref: "#/components/schemas/DocClient" + totalCount: + type: integer + format: int32 + description: Total number of clients. + minimum: 0 + maximum: 2147483647 + required: + - clients + - totalCount + ListDocuments: type: array description: The documents in the client. @@ -3362,6 +3458,189 @@ components: format: double minimum: 0 maximum: 9999999999.99 + # Grouper Extended Fields + aareteDerivedGrouperVersion: + type: string + maxLength: 200 + grouperAlternativeLevelOfCare: + type: string + maxLength: 200 + grouperSeverityInd: + type: boolean + grouperSeverity: + type: string + maxLength: 100 + grouperRiskOfMortalitySubclass: + type: string + maxLength: 100 + grouperTransferInd: + type: boolean + grouperReadmissionsInd: + type: boolean + grouperHacInd: + type: boolean + # Outlier Fields + outlierTerm: + type: string + maxLength: 500 + outlierFirstDollarInd: + type: boolean + rangeNbrDays: + type: string + maxLength: 100 + outlierFixedLossNbrDaysThreshold: + type: number + format: double + minimum: 0 + maximum: 99999999.99 + outlierFixedLossThreshold: + type: number + format: double + minimum: 0 + maximum: 9999999999.99 + outlierMaximum: + type: number + format: double + minimum: 0 + maximum: 9999999999.99 + outlierMaximumFrequency: + type: number + format: double + minimum: 0 + maximum: 99999999.99 + outlierPctRate: + type: number + format: double + minimum: 0 + maximum: 999.9999 + outlierExclusionCd: + type: string + maxLength: 100 + outlierExclusionCdDesc: + type: string + maxLength: 500 + # Facility Adjustment Fields + facilityAdjustmentTerm: + type: string + maxLength: 500 + dshInd: + type: boolean + dshPctRate: + type: number + format: double + minimum: 0 + maximum: 999.9999 + dshFeeRate: + type: number + format: double + minimum: 0 + maximum: 9999999999.99 + imeInd: + type: boolean + imePctRate: + type: number + format: double + minimum: 0 + maximum: 999.9999 + imeFeeRate: + type: number + format: double + minimum: 0 + maximum: 9999999999.99 + ntapInd: + type: boolean + ntapPctRate: + type: number + format: double + minimum: 0 + maximum: 999.9999 + ntapFeeRate: + type: number + format: double + minimum: 0 + maximum: 9999999999.99 + ucInd: + type: boolean + ucPctRate: + type: number + format: double + minimum: 0 + maximum: 999.9999 + ucFeeRate: + type: number + format: double + minimum: 0 + maximum: 9999999999.99 + gmeInd: + type: boolean + gmePctRate: + type: number + format: double + minimum: 0 + maximum: 999.9999 + gmeFeeRate: + type: number + format: double + minimum: 0 + maximum: 9999999999.99 + # Rate Escalator Fields + rateEscalatorInd: + type: boolean + rateEscalatorDesc: + type: string + maxLength: 500 + rateEscalatorMaxRateIncPct: + type: number + format: double + minimum: 0 + maximum: 999.9999 + rateEscalatorRateChangeTimeline: + type: number + format: double + minimum: 0 + maximum: 99999999.99 + # Stop Loss Fields + stopLossTerm: + type: string + maxLength: 500 + stopLossFirstDollarInd: + type: boolean + stopLossRangeNbrDays: + type: number + format: double + minimum: 0 + maximum: 99999999.99 + stopLossFixedLossThreshold: + type: number + format: double + minimum: 0 + maximum: 9999999999.99 + stopLossMaximum: + type: number + format: double + minimum: 0 + maximum: 9999999999.99 + stopLossMaximumFrequency: + type: number + format: double + minimum: 0 + maximum: 99999999.99 + stopLossDailyMaxRate: + type: number + format: double + minimum: 0 + maximum: 9999999999.99 + stopLossPctRateOnExcessCharges: + type: number + format: double + minimum: 0 + maximum: 999.9999 + stopLossExclusionCd: + type: string + maxLength: 100 + stopLossExclusionDesc: + type: string + maxLength: 500 FieldExtractionResponse: description: Field extraction with version information diff --git a/test/text_extraction_integration_test.go b/test/text_extraction_integration_test.go index 84365571..4e8a2e6c 100644 --- a/test/text_extraction_integration_test.go +++ b/test/text_extraction_integration_test.go @@ -37,6 +37,7 @@ type TextExtractionConfig struct { // Document Upload Phase: // 1. Create a new client via POST /client // 2. Verify client exists via GET /client/{id} +// 2b. List all clients via GET /clients and verify new client is present // 3. Update client to allow sync via PATCH /client/{id} // 4. Confirm client status via GET /client/{id}/status // 5. Create a ZIP with 3 PDFs in folder structures @@ -60,6 +61,7 @@ type TextExtractionConfig struct { // 20. Verify single extraction via GET /field-extractions/history // 21. Modify and create new version via POST /field-extractions // 22. Verify two versions via GET /field-extractions/history +// 23. Retrieve specific version via GET /field-extractions/version func TestE2EDocWorkflow(t *testing.T) { t.Run("docs", func(t *testing.T) { cfg := &TextExtractionConfig{} @@ -114,6 +116,23 @@ func runClientSetupPhase(t *testing.T, net test.Network) (string, string) { fmt.Printf(" Client verified: ID=%s, Name=%s\n", getClientRes.JSON200.Id, getClientRes.JSON200.Name) assert.Equal(t, clientName, getClientRes.JSON200.Name, "Client name mismatch") + fmt.Println("\n=== Step 2b: List all clients via GET /clients ===") + listClientsRes, err := net.Client.ListClientsWithResponse(t.Context()) + require.NoError(t, err, "Failed to list clients") + require.NotNil(t, listClientsRes.JSON200, "Expected 200 response, got status %d", listClientsRes.StatusCode()) + fmt.Printf(" Found %d clients in system\n", listClientsRes.JSON200.TotalCount) + // Verify our newly created client is in the list + foundClient := false + for _, c := range listClientsRes.JSON200.Clients { + if c.Id == clientID { + foundClient = true + assert.Equal(t, clientName, c.Name, "Client name in list doesn't match") + fmt.Printf(" Verified client %s is in the list\n", clientID) + break + } + } + require.True(t, foundClient, "Newly created client %s not found in list of clients", clientID) + fmt.Println("\n=== Step 3: Update client to allow sync ===") canSync := true updateClientRes, err := net.Client.UpdateClientWithResponse(t.Context(), clientID, queryapi.ClientUpdate{ @@ -337,7 +356,7 @@ func verifyDocumentsByLabel(t *testing.T, net test.Network, clientID string, lab } } -// runFieldExtractionsPhase handles steps 18-22: field extraction creation and versioning. +// runFieldExtractionsPhase handles steps 18-23: field extraction creation, versioning, and specific version retrieval. func runFieldExtractionsPhase(t *testing.T, net test.Network, targetDocID openapi_types.UUID) { t.Helper() @@ -355,6 +374,9 @@ func runFieldExtractionsPhase(t *testing.T, net test.Network, targetDocID openap fmt.Println("\n=== Step 22: Verify two versions in history ===") verifyFieldExtractionHistory(t, net, targetDocID, 2) + + fmt.Println("\n=== Step 23: Retrieve specific versions via GET /field-extractions/version ===") + verifySpecificVersionRetrieval(t, net, targetDocID) } // verifyNoFieldExtraction checks that no field extraction exists for the document. @@ -418,6 +440,52 @@ func verifyFieldExtractionHistory(t *testing.T, net test.Network, docID openapi_ } } +// verifySpecificVersionRetrieval tests the GET /field-extractions/version endpoint +// to retrieve specific versions of field extractions by document ID and version number. +func verifySpecificVersionRetrieval(t *testing.T, net test.Network, docID openapi_types.UUID) { + t.Helper() + + // Test retrieving version 1 + fmt.Println(" Testing retrieval of version 1...") + version1Res, err := net.Client.GetFieldExtractionByVersionWithResponse(t.Context(), &queryapi.GetFieldExtractionByVersionParams{ + DocumentId: docID, + Version: 1, + }) + require.NoError(t, err, "Failed to get field extraction version 1") + require.NotNil(t, version1Res.JSON200, "Expected 200 for version 1, got %d with body: %s", version1Res.StatusCode(), string(version1Res.Body)) + assert.Equal(t, int32(1), version1Res.JSON200.Version, "Expected version number 1") + assert.NotNil(t, version1Res.JSON200.SingleFields.ContractTitle, "Expected contractTitle in version 1") + if version1Res.JSON200.SingleFields.ContractTitle != nil { + assert.Equal(t, "Test Contract Title", *version1Res.JSON200.SingleFields.ContractTitle, "Expected original contract title in version 1") + } + fmt.Printf(" Version 1 retrieved: contractTitle=%s\n", *version1Res.JSON200.SingleFields.ContractTitle) + + // Test retrieving version 2 + fmt.Println(" Testing retrieval of version 2...") + version2Res, err := net.Client.GetFieldExtractionByVersionWithResponse(t.Context(), &queryapi.GetFieldExtractionByVersionParams{ + DocumentId: docID, + Version: 2, + }) + require.NoError(t, err, "Failed to get field extraction version 2") + require.NotNil(t, version2Res.JSON200, "Expected 200 for version 2, got %d with body: %s", version2Res.StatusCode(), string(version2Res.Body)) + assert.Equal(t, int32(2), version2Res.JSON200.Version, "Expected version number 2") + assert.NotNil(t, version2Res.JSON200.SingleFields.ContractTitle, "Expected contractTitle in version 2") + if version2Res.JSON200.SingleFields.ContractTitle != nil { + assert.Equal(t, "Updated Contract Title v2", *version2Res.JSON200.SingleFields.ContractTitle, "Expected updated contract title in version 2") + } + fmt.Printf(" Version 2 retrieved: contractTitle=%s\n", *version2Res.JSON200.SingleFields.ContractTitle) + + // Test retrieving non-existent version (should return 404) + fmt.Println(" Testing retrieval of non-existent version 999...") + version999Res, err := net.Client.GetFieldExtractionByVersionWithResponse(t.Context(), &queryapi.GetFieldExtractionByVersionParams{ + DocumentId: docID, + Version: 999, + }) + require.NoError(t, err, "Failed to query for non-existent version") + assert.NotNil(t, version999Res.JSON404, "Expected 404 for non-existent version 999, got status %d", version999Res.StatusCode()) + fmt.Println(" Non-existent version correctly returned 404") +} + // createTestZIP creates a ZIP file in memory containing 3 simple PDF files // in the specified folder structure. func createTestZIP(t *testing.T) *bytes.Buffer {