2026-07-16 01:06:43 +08:00
package pipeline
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"strings"
2026-07-16 02:00:56 +08:00
"time"
2026-07-16 01:06:43 +08:00
"chis_osi/contract"
"chis_osi/mapping"
"chis_osi/osi"
"chis_osi/source"
)
type Status string
const (
StatusDone Status = "done"
StatusRetry Status = "retry"
StatusFailed Status = "failed"
StatusManualReview Status = "manual_review"
)
type Action string
const (
ActionNone Action = "none"
ActionCreate Action = "create"
ActionUpdate Action = "update"
ActionSkip Action = "skip"
)
type Outcome struct {
2026-07-16 02:00:56 +08:00
Status Status `json:"status"`
Action Action `json:"action"`
Reason string `json:"reason,omitempty"`
ServiceID string `json:"serviceId,omitempty"`
ResponseCode string `json:"responseCode,omitempty"`
PHRIDHint string `json:"phrIdHint,omitempty"`
2026-07-16 01:06:43 +08:00
}
type UpsertEvent struct {
TraceID string
Status Status
Action Action
Reason string
ServiceID string
ResponseCode string
PHRIDHint string
}
type PHISStatusUpdate struct {
SourceRecordID string
BusinessID string
Status Status
Reason string
}
type UpdateTarget struct {
PHRID string
EMPIID string
CheckID string
IDCard string
ManaUnitID string
ManaDoctorID string
Status string
}
type Converter interface {
ConvertHealthRecord ( source . HealthRecordTask ) ( contract . HealthRecordCreate , [] mapping . ValidationError )
}
type ConverterFunc func ( source . HealthRecordTask ) ( contract . HealthRecordCreate , [] mapping . ValidationError )
func ( f ConverterFunc ) ConvertHealthRecord ( task source . HealthRecordTask ) ( contract . HealthRecordCreate , [] mapping . ValidationError ) {
return f ( task )
}
type HealthRecordClient interface {
FindHealthRecord ( context . Context , osi . FindHealthRecordQuery ) ([] contract . FindHealthRecord , osi . Result , error )
CreateHealthRecord ( context . Context , contract . HealthRecordCreate ) ( contract . HealthRecordSaveResult , osi . Result , error )
UpdateHealthRecord ( context . Context , contract . HealthRecordCreate ) ( contract . HealthRecordSaveResult , osi . Result , error )
}
type IdempotencyStore interface {
Acquire ( context . Context , string ) ( IdempotencyLease , error )
Complete ( context . Context , string , string ) error
Release ( context . Context , string , string ) error
}
type IdempotencyState string
const (
IdempotencyAcquired IdempotencyState = "acquired"
IdempotencyCompleted IdempotencyState = "completed"
IdempotencyInProgress IdempotencyState = "in_progress"
)
type IdempotencyLease struct {
State IdempotencyState
Token string
}
type NotificationError struct {
Event * UpsertEvent
Update * PHISStatusUpdate
PriorErr error
ReportErr error
StatusErr error
}
func ( e * NotificationError ) Error () string {
return "finish upsert side effects: " + e . Unwrap (). Error ()
}
func ( e * NotificationError ) Unwrap () error { return errors . Join ( e . PriorErr , e . ReportErr , e . StatusErr ) }
type ReportSink interface {
Publish ( context . Context , UpsertEvent ) error
}
type PHISStatusWriter interface {
WriteStatus ( context . Context , PHISStatusUpdate ) error
}
type Dependencies struct {
Converter Converter
Client HealthRecordClient
Idempotency IdempotencyStore
Reports ReportSink
PHISStatuses PHISStatusWriter
}
type HealthRecordUpsertService struct {
deps Dependencies
}
2026-07-16 02:00:56 +08:00
const idempotencyCleanupTimeout = 5 * time . Second
2026-07-16 01:06:43 +08:00
func NewHealthRecordUpsertService ( deps Dependencies ) * HealthRecordUpsertService {
return & HealthRecordUpsertService { deps : deps }
}
func ( s * HealthRecordUpsertService ) Upsert ( ctx context . Context , task source . HealthRecordTask ) ( Outcome , error ) {
if err := s . validateDependencies (); err != nil {
return Outcome {}, err
}
req , validationErrors := s . deps . Converter . ConvertHealthRecord ( task )
if len ( validationErrors ) > 0 {
return s . finish ( ctx , task , Outcome { Status : StatusFailed , Action : ActionNone , Reason : "mapping validation failed: " + validationErrors [ 0 ]. Field })
}
idempotencyKey := healthRecordIdempotencyKey ( req . HealthRecord . CheckID , req . HealthRecord . IDCard )
lease , err := s . deps . Idempotency . Acquire ( ctx , idempotencyKey )
if err != nil {
return s . finish ( ctx , task , Outcome { Status : StatusRetry , Action : ActionNone , Reason : "idempotency lookup failed" }, err )
}
if lease . State == IdempotencyCompleted {
return s . finish ( ctx , task , Outcome { Status : StatusDone , Action : ActionSkip , Reason : "already completed" })
}
if lease . State == IdempotencyInProgress {
return s . finish ( ctx , task , Outcome { Status : StatusRetry , Action : ActionSkip , Reason : "another delivery is in progress" })
}
if lease . State != IdempotencyAcquired || strings . TrimSpace ( lease . Token ) == "" {
return s . finish ( ctx , task , Outcome { Status : StatusRetry , Action : ActionNone , Reason : "invalid idempotency lease" })
}
records , queryResult , err := s . deps . Client . FindHealthRecord ( ctx , osi . FindHealthRecordQuery { IDCard : req . HealthRecord . IDCard })
if err != nil || ! queryResult . Success {
status := classifyFailure ( queryResult )
return s . finishAcquired ( ctx , task , idempotencyKey , lease . Token , Outcome { Status : status , Action : ActionNone , Reason : "CHIS query failed" , ServiceID : osi . ServiceIDJKDAFind , ResponseCode : queryResult . Code })
}
if records == nil {
return s . finishAcquired ( ctx , task , idempotencyKey , lease . Token , Outcome { Status : StatusFailed , Action : ActionNone , Reason : "CHIS query returned null or missing data" , ServiceID : osi . ServiceIDJKDAFind , ResponseCode : queryResult . Code })
}
if len ( records ) > 1 {
return s . finishAcquired ( ctx , task , idempotencyKey , lease . Token , Outcome { Status : StatusManualReview , Action : ActionNone , Reason : "multiple CHIS health records found" , ServiceID : osi . ServiceIDJKDAFind , ResponseCode : queryResult . Code })
}
if len ( records ) == 0 {
return s . create ( ctx , task , idempotencyKey , lease . Token , req )
}
target := newUpdateTarget ( records [ 0 ]. HealthRecord )
if reason := validateUpdateTarget ( target , req ); reason != "" {
return s . finishAcquired ( ctx , task , idempotencyKey , lease . Token , Outcome { Status : StatusManualReview , Action : ActionNone , Reason : reason , ServiceID : osi . ServiceIDJKDAFind , ResponseCode : queryResult . Code , PHRIDHint : maskIdentifier ( target . PHRID )})
}
req . BaseInfo . PHRID = target . PHRID
req . HealthRecord . PhrID = target . PHRID
return s . update ( ctx , task , idempotencyKey , lease . Token , req )
}
func ( s * HealthRecordUpsertService ) create ( ctx context . Context , task source . HealthRecordTask , key , token string , req contract . HealthRecordCreate ) ( Outcome , error ) {
saved , result , err := s . deps . Client . CreateHealthRecord ( ctx , req )
outcome := Outcome { Status : StatusDone , Action : ActionCreate , ServiceID : osi . ServiceIDJKDACreate , ResponseCode : result . Code , PHRIDHint : maskIdentifier ( saved . PhrID )}
if err != nil || ! result . Success {
outcome . Status = classifyFailure ( result )
outcome . Reason = "CHIS create failed"
return s . finishAcquired ( ctx , task , key , token , outcome )
}
return s . complete ( ctx , task , key , token , outcome )
}
func ( s * HealthRecordUpsertService ) update ( ctx context . Context , task source . HealthRecordTask , key , token string , req contract . HealthRecordCreate ) ( Outcome , error ) {
saved , result , err := s . deps . Client . UpdateHealthRecord ( ctx , req )
phrID := saved . PhrID
if phrID == "" {
phrID = req . HealthRecord . PhrID
}
outcome := Outcome { Status : StatusDone , Action : ActionUpdate , ServiceID : osi . ServiceIDJKDAUpdate , ResponseCode : result . Code , PHRIDHint : maskIdentifier ( phrID )}
if err != nil || ! result . Success {
outcome . Status = classifyFailure ( result )
outcome . Reason = "CHIS update failed"
return s . finishAcquired ( ctx , task , key , token , outcome )
}
return s . complete ( ctx , task , key , token , outcome )
}
func ( s * HealthRecordUpsertService ) complete ( ctx context . Context , task source . HealthRecordTask , key , token string , outcome Outcome ) ( Outcome , error ) {
2026-07-16 02:00:56 +08:00
cleanupCtx , cancel := idempotencyCleanupContext ( ctx )
defer cancel ()
if err := s . deps . Idempotency . Complete ( cleanupCtx , key , token ); err != nil {
2026-07-16 01:06:43 +08:00
return outcome , fmt . Errorf ( "persist successful CHIS write before notification: %w" , err )
}
return s . finish ( ctx , task , outcome )
}
func ( s * HealthRecordUpsertService ) finishAcquired ( ctx context . Context , task source . HealthRecordTask , key , token string , outcome Outcome ) ( Outcome , error ) {
2026-07-16 02:00:56 +08:00
cleanupCtx , cancel := idempotencyCleanupContext ( ctx )
defer cancel ()
return s . finish ( ctx , task , outcome , s . deps . Idempotency . Release ( cleanupCtx , key , token ))
}
func idempotencyCleanupContext ( ctx context . Context ) ( context . Context , context . CancelFunc ) {
return context . WithTimeout ( context . WithoutCancel ( ctx ), idempotencyCleanupTimeout )
2026-07-16 01:06:43 +08:00
}
func ( s * HealthRecordUpsertService ) finish ( ctx context . Context , task source . HealthRecordTask , outcome Outcome , prior ... error ) ( Outcome , error ) {
event := UpsertEvent {
TraceID : traceID ( task . SourceRecordID (), task . BusinessID ), Status : outcome . Status ,
Action : outcome . Action , Reason : outcome . Reason , ServiceID : outcome . ServiceID ,
ResponseCode : outcome . ResponseCode , PHRIDHint : outcome . PHRIDHint ,
}
status := PHISStatusUpdate { SourceRecordID : task . SourceRecordID (), BusinessID : task . BusinessID , Status : outcome . Status , Reason : outcome . Reason }
priorErr := errors . Join ( prior ... )
reportErr := s . deps . Reports . Publish ( ctx , event )
statusErr := s . deps . PHISStatuses . WriteStatus ( ctx , status )
if priorErr != nil || reportErr != nil || statusErr != nil {
notificationErr := & NotificationError { PriorErr : priorErr , ReportErr : reportErr , StatusErr : statusErr }
if reportErr != nil {
notificationErr . Event = & event
}
if statusErr != nil {
notificationErr . Update = & status
}
return outcome , notificationErr
}
return outcome , nil
}
func ( s * HealthRecordUpsertService ) validateDependencies () error {
if s == nil || s . deps . Converter == nil || s . deps . Client == nil || s . deps . Idempotency == nil || s . deps . Reports == nil || s . deps . PHISStatuses == nil {
return fmt . Errorf ( "health record upsert dependencies are incomplete" )
}
return nil
}
func classifyFailure ( result osi . Result ) Status {
if result . Retryable {
return StatusRetry
}
return StatusFailed
}
func newUpdateTarget ( record contract . HealthRecord ) UpdateTarget {
checkID := ""
if record . CheckID != nil {
checkID = * record . CheckID
}
return UpdateTarget {
PHRID : record . PhrID , EMPIID : record . EmpiID , CheckID : checkID , IDCard : record . IDCard ,
ManaUnitID : record . ManaUnitID , ManaDoctorID : record . ManaDoctorID , Status : record . Status ,
}
}
func validateUpdateTarget ( target UpdateTarget , req contract . HealthRecordCreate ) string {
if strings . TrimSpace ( target . IDCard ) != strings . TrimSpace ( req . HealthRecord . IDCard ) {
return "CHIS identity does not exactly match source"
}
if strings . TrimSpace ( target . PHRID ) == "" {
return "CHIS record is missing phrId"
}
if strings . TrimSpace ( target . Status ) != "1" {
return "CHIS record status is not updateable"
}
if strings . TrimSpace ( target . ManaUnitID ) != strings . TrimSpace ( req . HealthRecord . ManaUnitID ) {
return "CHIS record belongs to another organization"
}
if strings . TrimSpace ( target . ManaDoctorID ) != strings . TrimSpace ( req . HealthRecord . ManaDoctorID ) {
return "CHIS record belongs to another doctor"
}
return ""
}
func healthRecordIdempotencyKey ( checkID , idCard string ) string {
sum := sha256 . Sum256 ([] byte ( strings . TrimSpace ( checkID ) + "|" + strings . TrimSpace ( idCard )))
return fmt . Sprintf ( "health-record:%x" , sum [:])
}
func traceID ( sourceRecordID , businessID string ) string {
sum := sha256 . Sum256 ([] byte ( strings . TrimSpace ( sourceRecordID ) + "|" + strings . TrimSpace ( businessID )))
return fmt . Sprintf ( "hr-%x" , sum [: 8 ])
}
func maskIdentifier ( value string ) string {
value = strings . TrimSpace ( value )
if len ( value ) <= 4 {
return "****"
}
return "****" + value [ len ( value ) - 4 :]
}