From 6e1f049561a733f17fd1c849f7e559e8423453da Mon Sep 17 00:00:00 2001 From: Will Edgington Date: Tue, 10 Dec 2024 16:25:27 -0500 Subject: [PATCH] Add VisitNoteServicer.Find and return ErrBillExist on create if one already exists for a visit note --- bill.go | 73 +++++++++++++++++++++++------------- bill_test.go | 50 +++++++++++++++++++++++++ visit_note.go | 34 +++++++++++++++++ visit_note_test.go | 92 ++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 223 insertions(+), 26 deletions(-) diff --git a/bill.go b/bill.go index c536ec5..480e4e1 100644 --- a/bill.go +++ b/bill.go @@ -2,14 +2,21 @@ package elation import ( "context" + "encoding/json" + "errors" "fmt" "net/http" + "slices" "time" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/trace" ) +const billExistError = "The visit note provided already has a bill associated with it." + +var ErrBillExist = errors.New("bill already exists for visit note") + type BillServicer interface { Create(ctx context.Context, create *BillCreate) (*Bill, *http.Response, error) } @@ -21,32 +28,32 @@ type BillService struct { } type Bill struct { - ID int64 `json:"id"` //: 65099661468, - RefNumber *string `json:"ref_number"` //: null, // string(50). required for PATCH that marks bill as processed. - ServiceDate time.Time `json:"service_date"` //: "2016-10-12T12:00:00Z", - BillingDate *time.Time `json:"billing_date"` //: null, // datetime(iso8601). required for PATCH that marks bill as processed. - BillingStatus string `json:"billing_status"` //: "Unbilled", - BillingError *string `json:"billing_error"` //: null, // string(200). required for PATCH that marks bill as failed. - BillingRawError *string `json:"billing_raw_error"` //: null, // longtext. optional for PATCH that marks bill as failed. - Notes string `json:"notes"` //: "patient has not paid yet", - CPTs []*BillCPT `json:"cpts"` //: [{}], - Payment int64 `json:"payment"` //: 142502884606313, - VisitNote int64 `json:"visit_note"` //: 64409108504, - VisitNoteSignedDate time.Time `json:"visit_note_signed_date"` //: "2016-10-12T22:11:01Z", - VisitNoteDeletedDate *time.Time `json:"visit_note_deleted_date"` //: null, - ReferringProvider *BillProvider `json:"referring_provider"` //: {}, - BillingProvider *int64 `json:"billing_provider"` //: 42120898, - RenderingProvider *int64 `json:"rendering_provider"` //: 68382673, - SupervisingProvider *int64 `json:"supervising_provider"` //: 52893234, - OrderingProvider *BillProvider `json:"ordering_provider"` //: {} - ServiceLocation int64 `json:"service_location"` //: 141103949480183, - Physician int64 `json:"physician"` //: 64811630594, - Practice int64 `json:"practice"` //: 65540, - Patient int64 `json:"patient"` //: 64901939201, - PriorAuthorization *string `json:"prior_authorization"` //: "1234-ABC", - Metadata any `json:"metadata"` //: null, - CreatedDate time.Time `json:"created_date"` //: "2016-05-23T17:50:50Z", - LastModifiedDate time.Time `json:"last_modified_date"` //: "2016-10-12T22:39:46Z" + ID int64 `json:"id"` //: 65099661468, + RefNumber *string `json:"ref_number"` //: null, // string(50). required for PATCH that marks bill as processed. + ServiceDate time.Time `json:"service_date"` //: "2016-10-12T12:00:00Z", + BillingDate *time.Time `json:"billing_date"` //: null, // datetime(iso8601). required for PATCH that marks bill as processed. + BillingStatus string `json:"billing_status"` //: "Unbilled", + BillingError *string `json:"billing_error"` //: null, // string(200). required for PATCH that marks bill as failed. + BillingRawError *string `json:"billing_raw_error"` //: null, // longtext. optional for PATCH that marks bill as failed. + Notes string `json:"notes"` //: "patient has not paid yet", + CPTs []*BillCPT `json:"cpts"` //: [{}], + Payment int64 `json:"payment"` //: 142502884606313, + VisitNote int64 `json:"visit_note"` //: 64409108504, + VisitNoteSignedDate time.Time `json:"visit_note_signed_date"` //: "2016-10-12T22:11:01Z", + VisitNoteDeletedDate *time.Time `json:"visit_note_deleted_date"` //: null, + ReferringProvider *BillProvider `json:"referring_provider"` //: {}, + BillingProvider *int64 `json:"billing_provider"` //: 42120898, + RenderingProvider *int64 `json:"rendering_provider"` //: 68382673, + SupervisingProvider *int64 `json:"supervising_provider"` //: 52893234, + OrderingProvider *BillProvider `json:"ordering_provider"` //: {} + ServiceLocation json.RawMessage `json:"service_location"` //: 141103949480183, + Physician int64 `json:"physician"` //: 64811630594, + Practice int64 `json:"practice"` //: 65540, + Patient int64 `json:"patient"` //: 64901939201, + PriorAuthorization *string `json:"prior_authorization"` //: "1234-ABC", + Metadata any `json:"metadata"` //: null, + CreatedDate time.Time `json:"created_date"` //: "2016-05-23T17:50:50Z", + LastModifiedDate time.Time `json:"last_modified_date"` //: "2016-10-12T22:39:46Z" } type BillCreate struct { @@ -115,6 +122,20 @@ func (b *BillService) Create(ctx context.Context, create *BillCreate) (*Bill, *h if err != nil { span.RecordError(err) span.SetStatus(codes.Error, "error making request") + + var clientErr *Error + if errors.As(err, &clientErr) && clientErr.StatusCode == http.StatusBadRequest { + errorRes := map[string][]string{} + err := json.Unmarshal([]byte(clientErr.Body), &errorRes) + if err != nil { + return nil, res, fmt.Errorf("unmarshaling response body to error response: %w", err) + } + + if len(errorRes["visit_note"]) > 0 && slices.Contains(errorRes["visit_note"], billExistError) { + return nil, res, ErrBillExist + } + } + return nil, res, fmt.Errorf("making request: %w", err) } diff --git a/bill_test.go b/bill_test.go index 9d67996..e6d8997 100644 --- a/bill_test.go +++ b/bill_test.go @@ -100,3 +100,53 @@ func TestBillService_Create(t *testing.T) { }) } } + +func TestBillService_Create_already_exists(t *testing.T) { + assert := assert.New(t) + + billCreate := &BillCreate{ + ServiceLocation: 10, + VisitNote: 64409108504, + Patient: 64901939201, + Practice: 65540, + Physician: 64811630594, + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if tokenRequest(w, r) { + return + } + + assert.Equal(http.MethodPost, r.Method) + assert.Equal("/bills", r.URL.Path) + + body, err := io.ReadAll(r.Body) + assert.NoError(err) + + actualBillCreate := &BillCreate{} + err = json.Unmarshal(body, actualBillCreate) + assert.NoError(err) + + assert.Equal(billCreate, actualBillCreate) + + errorRes := map[string][]string{ + "visit_note": {billExistError}, + } + b, err := json.Marshal(errorRes) + assert.NoError(err) + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + //nolint + w.Write(b) + })) + defer srv.Close() + + client := NewHTTPClient(srv.Client(), srv.URL+"/token", "", "", srv.URL) + svc := BillService{client} + + created, res, err := svc.Create(context.Background(), billCreate) + assert.Nil(created) + assert.NotNil(res) + assert.ErrorIs(err, ErrBillExist) +} diff --git a/visit_note.go b/visit_note.go index 6091e12..d93ed83 100644 --- a/visit_note.go +++ b/visit_note.go @@ -12,6 +12,7 @@ import ( type VisitNoteServicer interface { Create(ctx context.Context, create *VisitNoteCreate) (*VisitNote, *http.Response, error) + Find(ctx context.Context, opts *FindVisitNotesOptions) (*Response[[]*VisitNote], *http.Response, error) } var _ VisitNoteServicer = (*VisitNoteService)(nil) @@ -178,3 +179,36 @@ func (v *VisitNoteService) Create(ctx context.Context, create *VisitNoteCreate) return vn, res, nil } + +type FindVisitNotesOptions struct { + *Pagination + + Patient int64 `url:"patient,omitempty"` + Physician int64 `url:"physician,omitempty"` + Practice int64 `url:"practice,omitempty"` + + LastModifiedGT time.Time `url:"last_modified_gt,omitempty"` + LastModifiedGTE time.Time `url:"last_modified_gte,omitempty"` + LastModifiedLT time.Time `url:"last_modified_lt,omitempty"` + LastModifiedLTE time.Time `url:"last_modified_lte,omitempty"` + + FromSignedDate time.Time `url:"from_signed_date,omitempty"` + ToSignedDate time.Time `url:"to_signed_date,omitempty"` + Unsigned bool `url:"unsigned,omitempty"` +} + +func (v *VisitNoteService) Find(ctx context.Context, opts *FindVisitNotesOptions) (*Response[[]*VisitNote], *http.Response, error) { + ctx, span := v.client.tracer.Start(ctx, "find visit notes", trace.WithSpanKind(trace.SpanKindClient)) + defer span.End() + + out := &Response[[]*VisitNote]{} + + res, err := v.client.request(ctx, http.MethodGet, "/visit_notes", opts, nil, &out) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, "error making request") + return nil, res, fmt.Errorf("making request: %w", err) + } + + return out, res, nil +} diff --git a/visit_note_test.go b/visit_note_test.go index e9fa853..b8bae96 100644 --- a/visit_note_test.go +++ b/visit_note_test.go @@ -105,3 +105,95 @@ func TestVisitNoteService_Create(t *testing.T) { }) } } + +func TestVisitNoteService_Find(t *testing.T) { + assert := assert.New(t) + + opts := &FindVisitNotesOptions{ + Patient: 123, + Physician: 456, + Practice: 789, + LastModifiedGT: time.Date(2022, 1, 1, 0, 0, 0, 0, time.UTC), + LastModifiedGTE: time.Date(2022, 1, 2, 0, 0, 0, 0, time.UTC), + LastModifiedLT: time.Date(2022, 1, 3, 0, 0, 0, 0, time.UTC), + LastModifiedLTE: time.Date(2022, 1, 4, 0, 0, 0, 0, time.UTC), + FromSignedDate: time.Date(2022, 1, 5, 0, 0, 0, 0, time.UTC), + ToSignedDate: time.Date(2022, 1, 6, 0, 0, 0, 0, time.UTC), + Unsigned: true, + } + + visitNotes := []*VisitNote{ + { + ID: 1, + }, + { + ID: 2, + }, + } + + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if tokenRequest(w, r) { + return + } + + assert.Equal(http.MethodGet, r.Method) + assert.Equal("/visit_notes", r.URL.Path) + + actualPatient := r.URL.Query().Get("patient") + assert.Equal(opts.Patient, strToInt64(actualPatient)) + + actualPhysician := r.URL.Query().Get("physician") + assert.Equal(opts.Physician, strToInt64(actualPhysician)) + + actualPractice := r.URL.Query().Get("practice") + assert.Equal(opts.Practice, strToInt64(actualPractice)) + + actualLastModifiedGT := r.URL.Query().Get("last_modified_gt") + assert.Equal(opts.LastModifiedGT.Format(time.RFC3339), actualLastModifiedGT) + + actualLastModifiedGTE := r.URL.Query().Get("last_modified_gte") + assert.Equal(opts.LastModifiedGTE.Format(time.RFC3339), actualLastModifiedGTE) + + actualLastModifiedLT := r.URL.Query().Get("last_modified_lt") + assert.Equal(opts.LastModifiedLT.Format(time.RFC3339), actualLastModifiedLT) + + actualLastModifiedLTE := r.URL.Query().Get("last_modified_lte") + assert.Equal(opts.LastModifiedLTE.Format(time.RFC3339), actualLastModifiedLTE) + + actualFromSignedDate := r.URL.Query().Get("from_signed_date") + assert.Equal(opts.FromSignedDate.Format(time.RFC3339), actualFromSignedDate) + + actualToSignedDate := r.URL.Query().Get("to_signed_date") + assert.Equal(opts.ToSignedDate.Format(time.RFC3339), actualToSignedDate) + + actualUnsigned := r.URL.Query().Get("unsigned") + assert.Equal(opts.Unsigned, strToBool(actualUnsigned)) + + b, err := json.Marshal(Response[[]*VisitNote]{ + Results: []*VisitNote{ + { + ID: 1, + }, + { + ID: 2, + }, + }, + }) + assert.NoError(err) + + w.Header().Set("Content-Type", "application/json") + //nolint + w.Write(b) + })) + defer srv.Close() + + client := NewHTTPClient(srv.Client(), srv.URL+"/token", "", "", srv.URL) + svc := VisitNoteService{client} + + visitNotesRes, res, err := svc.Find(context.Background(), opts) + assert.NotEmpty(visitNotesRes) + assert.NotNil(res) + assert.NoError(err) + + assert.Equal(visitNotes, visitNotesRes.Results) +}