package pipeline import ( "context" "errors" "testing" "chis_osi/contract" "chis_osi/mapping" "chis_osi/osi" "chis_osi/source" ) func TestHealthRecordUpsertCreatesWhenQueryIsEmpty(t *testing.T) { client := &fakeHealthRecordClient{rows: []contract.FindHealthRecord{}, findResult: osi.Result{Success: true}, saveResult: osi.Result{Success: true, Code: "01"}, saved: contract.HealthRecordSaveResult{PhrID: "PHR-NEW"}} store := &fakeIdempotencyStore{} reports := &fakeReportSink{} statuses := &fakeStatusWriter{} service := newTestService(client, store, reports, statuses) outcome, err := service.Upsert(context.Background(), testTask()) if err != nil || outcome.Status != StatusDone || outcome.Action != ActionCreate { t.Fatalf("outcome=%#v err=%v", outcome, err) } if client.findCalls != 1 || client.createCalls != 1 || client.updateCalls != 0 || !store.marked || !store.completeHasDeadline { t.Fatalf("calls find=%d create=%d update=%d marked=%v deadline=%v", client.findCalls, client.createCalls, client.updateCalls, store.marked, store.completeHasDeadline) } assertNotifications(t, reports, statuses, StatusDone, ActionCreate) } func TestHealthRecordUpsertUpdatesOneMatchingRecord(t *testing.T) { client := &fakeHealthRecordClient{ findResult: osi.Result{Success: true}, rows: []contract.FindHealthRecord{{HealthRecord: matchingTarget()}}, saveResult: osi.Result{Success: true, Code: "01"}, saved: contract.HealthRecordSaveResult{PhrID: "PHR-EXISTING"}, } service := newTestService(client, &fakeIdempotencyStore{}, &fakeReportSink{}, &fakeStatusWriter{}) outcome, err := service.Upsert(context.Background(), testTask()) if err != nil || outcome.Status != StatusDone || outcome.Action != ActionUpdate { t.Fatalf("outcome=%#v err=%v", outcome, err) } if client.updateCalls != 1 || client.createCalls != 0 { t.Fatalf("create=%d update=%d", client.createCalls, client.updateCalls) } if client.updateReq.BaseInfo.PHRID != "PHR-EXISTING" || client.updateReq.HealthRecord.PhrID != "PHR-EXISTING" { t.Fatalf("update phrId not merged: %#v", client.updateReq) } if client.updateReq.HealthRecord.CheckID != "CHECK-STABLE" { t.Fatalf("source checkId was overwritten: %q", client.updateReq.HealthRecord.CheckID) } } func TestHealthRecordUpsertRoutesUnsafeMatchesToManualReview(t *testing.T) { tests := []struct { name string rows []contract.FindHealthRecord }{ {name: "multiple", rows: []contract.FindHealthRecord{{HealthRecord: matchingTarget()}, {HealthRecord: matchingTarget()}}}, {name: "cross unit", rows: []contract.FindHealthRecord{{HealthRecord: targetWith(func(r *contract.HealthRecord) { r.ManaUnitID = "UNIT-OTHER" })}}}, {name: "cross doctor", rows: []contract.FindHealthRecord{{HealthRecord: targetWith(func(r *contract.HealthRecord) { r.ManaDoctorID = "DOC-OTHER" })}}}, {name: "inactive", rows: []contract.FindHealthRecord{{HealthRecord: targetWith(func(r *contract.HealthRecord) { r.Status = "0" })}}}, {name: "id mismatch", rows: []contract.FindHealthRecord{{HealthRecord: targetWith(func(r *contract.HealthRecord) { r.IDCard = "440000********9999" })}}}, {name: "missing phrId", rows: []contract.FindHealthRecord{{HealthRecord: targetWith(func(r *contract.HealthRecord) { r.PhrID = "" })}}}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := &fakeHealthRecordClient{rows: tt.rows, findResult: osi.Result{Success: true}} service := newTestService(client, &fakeIdempotencyStore{}, &fakeReportSink{}, &fakeStatusWriter{}) outcome, err := service.Upsert(context.Background(), testTask()) if err != nil || outcome.Status != StatusManualReview || client.createCalls != 0 || client.updateCalls != 0 { t.Fatalf("outcome=%#v err=%v create=%d update=%d", outcome, err, client.createCalls, client.updateCalls) } }) } } func TestHealthRecordUpsertNeverCreatesAfterQueryFailure(t *testing.T) { for _, retryable := range []bool{false, true} { client := &fakeHealthRecordClient{findResult: osi.Result{Retryable: retryable, Code: "405"}, findErr: errors.New("query failed")} store := &fakeIdempotencyStore{} service := newTestService(client, store, &fakeReportSink{}, &fakeStatusWriter{}) outcome, err := service.Upsert(context.Background(), testTask()) want := StatusFailed if retryable { want = StatusRetry } if err != nil || outcome.Status != want || client.createCalls != 0 || client.updateCalls != 0 || !store.released || !store.releaseHasDeadline { t.Fatalf("retryable=%v outcome=%#v err=%v", retryable, outcome, err) } } } func TestHealthRecordUpsertNeverCreatesForNullQueryData(t *testing.T) { client := &fakeHealthRecordClient{rows: nil, findResult: osi.Result{Success: true, Code: "01"}} service := newTestService(client, &fakeIdempotencyStore{}, &fakeReportSink{}, &fakeStatusWriter{}) outcome, err := service.Upsert(context.Background(), testTask()) if err != nil || outcome.Status != StatusFailed || client.createCalls != 0 { t.Fatalf("outcome=%#v err=%v create=%d", outcome, err, client.createCalls) } } func TestHealthRecordUpsertClassifiesWriteFailureWithoutFallback(t *testing.T) { tests := []struct { name string rows []contract.FindHealthRecord retryable bool action Action }{ {name: "create retry", rows: []contract.FindHealthRecord{}, retryable: true, action: ActionCreate}, {name: "create fail", rows: []contract.FindHealthRecord{}, action: ActionCreate}, {name: "update retry", rows: []contract.FindHealthRecord{{HealthRecord: matchingTarget()}}, retryable: true, action: ActionUpdate}, {name: "update fail", rows: []contract.FindHealthRecord{{HealthRecord: matchingTarget()}}, action: ActionUpdate}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { client := &fakeHealthRecordClient{rows: tt.rows, findResult: osi.Result{Success: true}, saveResult: osi.Result{Retryable: tt.retryable}, saveErr: errors.New("write failed")} service := newTestService(client, &fakeIdempotencyStore{}, &fakeReportSink{}, &fakeStatusWriter{}) outcome, err := service.Upsert(context.Background(), testTask()) want := StatusFailed if tt.retryable { want = StatusRetry } if err != nil || outcome.Status != want || outcome.Action != tt.action { t.Fatalf("outcome=%#v err=%v", outcome, err) } if client.createCalls+client.updateCalls != 1 { t.Fatalf("unexpected write fallback: create=%d update=%d", client.createCalls, client.updateCalls) } }) } } func TestHealthRecordUpsertSkipsCompletedIdempotencyKey(t *testing.T) { client := &fakeHealthRecordClient{} service := newTestService(client, &fakeIdempotencyStore{completed: true}, &fakeReportSink{}, &fakeStatusWriter{}) outcome, err := service.Upsert(context.Background(), testTask()) if err != nil || outcome.Status != StatusDone || outcome.Action != ActionSkip || client.findCalls != 0 { t.Fatalf("outcome=%#v err=%v find=%d", outcome, err, client.findCalls) } } func TestHealthRecordUpsertDoesNotRaceAnInProgressDelivery(t *testing.T) { client := &fakeHealthRecordClient{} service := newTestService(client, &fakeIdempotencyStore{inProgress: true}, &fakeReportSink{}, &fakeStatusWriter{}) outcome, err := service.Upsert(context.Background(), testTask()) if err != nil || outcome.Status != StatusRetry || outcome.Action != ActionSkip || client.findCalls != 0 { t.Fatalf("outcome=%#v err=%v find=%d", outcome, err, client.findCalls) } } func TestHealthRecordUpsertRejectsMappingErrorsBeforeOSI(t *testing.T) { client := &fakeHealthRecordClient{} service := NewHealthRecordUpsertService(Dependencies{ Converter: ConverterFunc(func(source.HealthRecordTask) (contract.HealthRecordCreate, []mapping.ValidationError) { return contract.HealthRecordCreate{}, []mapping.ValidationError{{Field: "idCard", Reason: "required"}} }), Client: client, Idempotency: &fakeIdempotencyStore{}, Reports: &fakeReportSink{}, PHISStatuses: &fakeStatusWriter{}, }) outcome, err := service.Upsert(context.Background(), testTask()) if err != nil || outcome.Status != StatusFailed || client.findCalls != 0 { t.Fatalf("outcome=%#v err=%v find=%d", outcome, err, client.findCalls) } } func TestHealthRecordUpsertDoesNotNotifyDoneWhenCompletionFails(t *testing.T) { client := &fakeHealthRecordClient{rows: []contract.FindHealthRecord{}, findResult: osi.Result{Success: true}, saveResult: osi.Result{Success: true}} store := &fakeIdempotencyStore{completeErr: errors.New("store unavailable")} reports := &fakeReportSink{} statuses := &fakeStatusWriter{} service := newTestService(client, store, reports, statuses) outcome, err := service.Upsert(context.Background(), testTask()) if err == nil || outcome.Status != StatusDone || len(reports.events) != 0 || len(statuses.updates) != 0 { t.Fatalf("outcome=%#v err=%v reports=%#v statuses=%#v", outcome, err, reports.events, statuses.updates) } } func TestHealthRecordUpsertNotificationErrorOnlyCarriesFailedSink(t *testing.T) { client := &fakeHealthRecordClient{rows: []contract.FindHealthRecord{}, findResult: osi.Result{Success: true}, saveResult: osi.Result{Success: true}} statuses := &fakeStatusWriter{err: errors.New("PHIS unavailable")} service := newTestService(client, &fakeIdempotencyStore{}, &fakeReportSink{}, statuses) outcome, err := service.Upsert(context.Background(), testTask()) var notificationErr *NotificationError if outcome.Status != StatusDone || !errors.As(err, ¬ificationErr) { t.Fatalf("outcome=%#v err=%v", outcome, err) } if notificationErr.Event != nil || notificationErr.Update == nil || notificationErr.ReportErr != nil || notificationErr.StatusErr == nil { t.Fatalf("notification error = %#v", notificationErr) } } func newTestService(client *fakeHealthRecordClient, store *fakeIdempotencyStore, reports *fakeReportSink, statuses *fakeStatusWriter) *HealthRecordUpsertService { return NewHealthRecordUpsertService(Dependencies{ Converter: ConverterFunc(func(source.HealthRecordTask) (contract.HealthRecordCreate, []mapping.ValidationError) { return testRequest(), nil }), Client: client, Idempotency: store, Reports: reports, PHISStatuses: statuses, }) } func testTask() source.HealthRecordTask { return source.HealthRecordTask{ArchID: "ARCH-1", BusinessID: "BUS-1"} } func testRequest() contract.HealthRecordCreate { return contract.HealthRecordCreate{ BaseInfo: contract.HealthRecordBaseInfo{IDCard: "440000********1234", PersonName: "测试居民"}, ManageInfo: contract.ManageInfo{OperateUser: "DOC-1"}, HealthRecord: contract.HealthRecordCreateInfo{CheckID: "CHECK-STABLE", IDCard: "440000********1234", ManaUnitID: "UNIT-1", ManaDoctorID: "DOC-1"}, } } func matchingTarget() contract.HealthRecord { return contract.HealthRecord{IDCard: "440000********1234", PhrID: "PHR-EXISTING", EmpiID: "EMPI-1", ManaUnitID: "UNIT-1", ManaDoctorID: "DOC-1", Status: "1"} } func targetWith(change func(*contract.HealthRecord)) contract.HealthRecord { r := matchingTarget() change(&r) return r } type fakeHealthRecordClient struct { rows []contract.FindHealthRecord findResult, saveResult osi.Result findErr, saveErr error saved contract.HealthRecordSaveResult findCalls, createCalls, updateCalls int updateReq contract.HealthRecordCreate } func (f *fakeHealthRecordClient) FindHealthRecord(context.Context, osi.FindHealthRecordQuery) ([]contract.FindHealthRecord, osi.Result, error) { f.findCalls++ return f.rows, f.findResult, f.findErr } func (f *fakeHealthRecordClient) CreateHealthRecord(context.Context, contract.HealthRecordCreate) (contract.HealthRecordSaveResult, osi.Result, error) { f.createCalls++ return f.saved, f.saveResult, f.saveErr } func (f *fakeHealthRecordClient) UpdateHealthRecord(_ context.Context, req contract.HealthRecordCreate) (contract.HealthRecordSaveResult, osi.Result, error) { f.updateCalls++ f.updateReq = req return f.saved, f.saveResult, f.saveErr } type fakeIdempotencyStore struct { completed, inProgress, marked, released bool completeHasDeadline, releaseHasDeadline bool completeErr error } func (f *fakeIdempotencyStore) Acquire(context.Context, string) (IdempotencyLease, error) { if f.completed { return IdempotencyLease{State: IdempotencyCompleted}, nil } if f.inProgress { return IdempotencyLease{State: IdempotencyInProgress}, nil } return IdempotencyLease{State: IdempotencyAcquired, Token: "lease-1"}, nil } func (f *fakeIdempotencyStore) Complete(ctx context.Context, _, token string) error { _, f.completeHasDeadline = ctx.Deadline() if token != "lease-1" { return errors.New("stale lease") } f.marked = true return f.completeErr } func (f *fakeIdempotencyStore) Release(ctx context.Context, _, token string) error { _, f.releaseHasDeadline = ctx.Deadline() if token != "lease-1" { return errors.New("stale lease") } f.released = true return nil } type fakeReportSink struct{ events []UpsertEvent } func (f *fakeReportSink) Publish(_ context.Context, event UpsertEvent) error { f.events = append(f.events, event) return nil } type fakeStatusWriter struct { updates []PHISStatusUpdate err error } func (f *fakeStatusWriter) WriteStatus(_ context.Context, update PHISStatusUpdate) error { f.updates = append(f.updates, update) return f.err } func assertNotifications(t *testing.T, reports *fakeReportSink, statuses *fakeStatusWriter, status Status, action Action) { t.Helper() if len(reports.events) != 1 || reports.events[0].Status != status || reports.events[0].Action != action { t.Fatalf("reports=%#v", reports.events) } if len(statuses.updates) != 1 || statuses.updates[0].Status != status { t.Fatalf("statuses=%#v", statuses.updates) } }