package mapping import ( "strconv" "strings" "time" "unicode/utf8" "chis_osi/contract" "chis_osi/source" ) type PHISHealthRecord struct { SourceSystem string SourceRecordID string UpdatedAt string IDCard string PersonName string SexCode string Birthday string MobileNumber string RegionCode string RegionName string ManaDoctorID string ManaDoctorName string ManaUnitID string ManaUnitName string NationCode string EducationCode string WorkCode string MaritalStatusCode string BloodTypeCode string RhBloodCode string InsuranceCode string WorkPlace string Contact string ContactPhone string RegisteredPermanent string Address string AddressNumber string HomePlace string HomePlaceNumber string CardType string IsFillSHHJ string CreateDate string DrugAllergy string ExposureHistory string FamilyFather string FamilyMother string FamilySiblings string FamilyChildren string GeneticDisease string Disability string PastDiseaseCode string PastSurgeryCode string PastTraumaCode string PastTransfusionCode string CookAirTool string FuelType string WaterSourceCode string Washroom string LivestockColumn string } type MappedHealthRecord struct { CheckID string IDCard string PersonName string SexCode string Birthday string MobileNumber string RegionCode string ManaDoctorID string ManaUnitID string NationCode string EducationCode string WorkCode string MaritalStatusCode string BloodTypeCode string RhBloodCode string InsuranceCode string WorkPlace string Contact string ContactPhone string RegisteredPermanent string Address string AddressNumber string HomePlace string HomePlaceNumber string CardType string IsFillSHHJ string CreateDate string PastHistory *contract.PastHistory PastDiseases []contract.PastDisease PastSurgeries []contract.PastSurgery PastTraumas []contract.PastTrauma PastTransfusions []contract.PastBloodTx FamilyMiddle *contract.FamilyMiddle } type DictSnapshot struct { Sex Dict Nation Dict Education Dict Work Dict MaritalStatus Dict BloodType Dict RhBlood Dict Insurance Dict } type MasterDataLookup interface { RegionCodeByName(name string) (string, bool) DoctorIDByName(name string) (string, bool) ManaUnitIDByName(name string) (string, bool) DoctorIDExists(id string) bool ManaUnitIDExists(id string) bool } type MapContext struct { Dicts DictSnapshot MasterData MasterDataLookup } func DefaultDictSnapshot() DictSnapshot { return DictSnapshot{ Sex: SexDict, Nation: NationDict, Education: EducationDict, Work: WorkDict, MaritalStatus: MaritalStatusDict, BloodType: BloodTypeDict, RhBlood: RhBloodDict, Insurance: InsuranceDict, } } func MapHealthRecord(src PHISHealthRecord, ctx MapContext) (MappedHealthRecord, []ValidationError) { if ctx.Dicts.Sex.Name == "" { ctx.Dicts = DefaultDictSnapshot() } resolveMasterData(&src, ctx.MasterData) var errs []ValidationError for _, field := range []struct { name string value string }{ {name: "idCard", value: src.IDCard}, {name: "personName", value: src.PersonName}, {name: "sexCode", value: src.SexCode}, {name: "birthday", value: src.Birthday}, {name: "mobileNumber", value: src.MobileNumber}, {name: "regionCode", value: src.RegionCode}, {name: "manaDoctorId", value: src.ManaDoctorID}, {name: "manaUnitId", value: src.ManaUnitID}, } { if field.value == "" { errs = append(errs, ValidationError{Field: field.name, Reason: "required"}) } } mapped := MappedHealthRecord{ CheckID: GenerateCheckID(src.SourceSystem, "JKDA", src.SourceRecordID), IDCard: src.IDCard, PersonName: src.PersonName, Birthday: src.Birthday, MobileNumber: src.MobileNumber, RegionCode: src.RegionCode, ManaDoctorID: src.ManaDoctorID, ManaUnitID: src.ManaUnitID, WorkPlace: src.WorkPlace, Contact: src.Contact, ContactPhone: src.ContactPhone, RegisteredPermanent: src.RegisteredPermanent, Address: src.Address, AddressNumber: src.AddressNumber, HomePlace: src.HomePlace, HomePlaceNumber: src.HomePlaceNumber, CardType: src.CardType, IsFillSHHJ: strings.ToLower(strings.TrimSpace(src.IsFillSHHJ)), CreateDate: src.CreateDate, } mapped.SexCode = mapCode(ctx.Dicts.Sex, src.SexCode, &errs) mapped.NationCode = mapCode(ctx.Dicts.Nation, src.NationCode, &errs) mapped.EducationCode = mapCode(ctx.Dicts.Education, src.EducationCode, &errs) mapped.WorkCode = mapCode(ctx.Dicts.Work, src.WorkCode, &errs) mapped.MaritalStatusCode = mapCode(ctx.Dicts.MaritalStatus, src.MaritalStatusCode, &errs) mapped.BloodTypeCode = mapCode(ctx.Dicts.BloodType, src.BloodTypeCode, &errs) mapped.RhBloodCode = mapCode(ctx.Dicts.RhBlood, src.RhBloodCode, &errs) mapped.InsuranceCode = mapDelimitedCode(ctx.Dicts.Insurance, src.InsuranceCode, &errs) return mapped, errs } func MapHealthRecordTask(task source.HealthRecordTask, ctx MapContext) (contract.HealthRecordCreate, []ValidationError) { r := task.Record src := PHISHealthRecord{ SourceSystem: "PHIS", SourceRecordID: task.SourceRecordID(), UpdatedAt: r.UpdatedAt, IDCard: r.IDCard, PersonName: r.PersonName, SexCode: r.SexCode, Birthday: r.Birthday, MobileNumber: r.MobileNumber, RegionCode: r.RegionCode, ManaDoctorID: r.ManaDoctorID, ManaUnitID: r.ManaUnitID, NationCode: r.NationCode, EducationCode: r.EducationCode, WorkCode: r.WorkCode, MaritalStatusCode: r.MaritalStatusCode, BloodTypeCode: r.BloodTypeCode, RhBloodCode: r.RhBloodCode, InsuranceCode: r.InsuranceCode, WorkPlace: r.WorkPlace, Contact: r.Contact, ContactPhone: r.ContactPhone, RegisteredPermanent: r.RegisteredPermanent, Address: r.Address, AddressNumber: r.AddressNumber, HomePlace: r.HomePlace, HomePlaceNumber: r.HomePlaceNumber, CardType: firstNonEmpty(r.CardType, task.CardType), IsFillSHHJ: r.IsFillSHHJ, DrugAllergy: r.DrugAllergy, ExposureHistory: r.ExposureHistory, FamilyFather: r.FamilyFather, FamilyMother: r.FamilyMother, FamilySiblings: r.FamilySiblings, FamilyChildren: r.FamilyChildren, GeneticDisease: r.GeneticDisease, Disability: r.Disability, PastDiseaseCode: r.PastDiseaseCode, PastSurgeryCode: r.PastSurgeryCode, PastTraumaCode: r.PastTraumaCode, PastTransfusionCode: r.PastTransfusionCode, CookAirTool: r.CookAirTool, FuelType: r.FuelType, WaterSourceCode: r.WaterSourceCode, Washroom: r.Washroom, LivestockColumn: r.LivestockColumn, CreateDate: r.CreateDate, } mapped, errs := MapHealthRecord(src, ctx) errs = append(errs, validateTaskFields(task)...) errs = append(errs, validateMasterData(src, ctx.MasterData)...) pastHistory := &contract.PastHistory{} pastHistory.YWGMS = validateDelimitedCodes("ywgms", src.DrugAllergy, allergyCodes, &errs) pastHistory.BLS = validateDelimitedCodes("bls", src.ExposureHistory, exposureCodes, &errs) pastHistory.JZSFQN = validateDelimitedCodes("jzsfqn", src.FamilyFather, fatherCodes, &errs) pastHistory.JZSMQ = validateDelimitedCodes("jzsmq", src.FamilyMother, motherCodes, &errs) pastHistory.JZSXDJM = validateDelimitedCodes("jzsxdjm", src.FamilySiblings, siblingCodes, &errs) pastHistory.JZSZN = validateDelimitedCodes("jzszn", src.FamilyChildren, childrenCodes, &errs) pastHistory.YCBS = validateDelimitedCodes("ycbs", src.GeneticDisease, geneticCodes, &errs) pastHistory.CJQK = validateDelimitedCodes("cjqk", src.Disability, disabilityCodes, &errs) if *pastHistory != (contract.PastHistory{}) { mapped.PastHistory = pastHistory } mapped.PastDiseases = mapPastDiseases(validateDelimitedCodes("jwsjbcode", src.PastDiseaseCode, diseaseCodes, &errs)) mapped.PastSurgeries = mapPastSurgeries(validateDelimitedCodes("jwssscode", src.PastSurgeryCode, surgeryCodes, &errs)) mapped.PastTraumas = mapPastTraumas(validateDelimitedCodes("jwswscode", src.PastTraumaCode, traumaCodes, &errs)) mapped.PastTransfusions = mapPastTransfusions(validateDelimitedCodes("jwssxcode", src.PastTransfusionCode, transfusionCodes, &errs)) if mapped.IsFillSHHJ == "y" { mapped.FamilyMiddle = &contract.FamilyMiddle{ CookAirTool: validateSingleCode("cookAirTool", src.CookAirTool, cookAirCodes, &errs), FuelType: validateSingleCode("fuelType", src.FuelType, fuelCodes, &errs), WaterSourceCode: validateSingleCode("waterSourceCode", src.WaterSourceCode, waterCodes, &errs), Washroom: validateSingleCode("washroom", src.Washroom, washroomCodes, &errs), LivestockColumn: validateSingleCode("livestockColumn", src.LivestockColumn, livestockCodes, &errs), IsFillSHHJ: "y", } } return BuildHealthRecordCreate(mapped), errs } // ValidateHealthRecordTask performs all local mapping validation without // claiming that the supplied doctor or organization exists in CHIS. func ValidateHealthRecordTask(task source.HealthRecordTask) []ValidationError { _, errs := MapHealthRecordTask(task, MapContext{ Dicts: DefaultDictSnapshot(), MasterData: submittedHealthRecordMasterData{ doctorID: task.Record.ManaDoctorID, unitID: task.Record.ManaUnitID, }, }) return errs } type submittedHealthRecordMasterData struct { doctorID string unitID string } func (m submittedHealthRecordMasterData) RegionCodeByName(string) (string, bool) { return "", false } func (m submittedHealthRecordMasterData) DoctorIDByName(string) (string, bool) { return "", false } func (m submittedHealthRecordMasterData) ManaUnitIDByName(string) (string, bool) { return "", false } func (m submittedHealthRecordMasterData) DoctorIDExists(id string) bool { return strings.TrimSpace(id) != "" && strings.TrimSpace(id) == strings.TrimSpace(m.doctorID) } func (m submittedHealthRecordMasterData) ManaUnitIDExists(id string) bool { return strings.TrimSpace(id) != "" && strings.TrimSpace(id) == strings.TrimSpace(m.unitID) } func validateMasterData(src PHISHealthRecord, lookup MasterDataLookup) []ValidationError { if lookup == nil { return []ValidationError{{Field: "masterData", Reason: "CHIS dictionary snapshot is required"}} } var errs []ValidationError if src.ManaDoctorID != "" && !lookup.DoctorIDExists(src.ManaDoctorID) { errs = append(errs, ValidationError{Field: "manaDoctorId", Code: src.ManaDoctorID, Reason: "not found in CHIS doctor dictionary"}) } if src.ManaUnitID != "" && !lookup.ManaUnitIDExists(src.ManaUnitID) { errs = append(errs, ValidationError{Field: "manaUnitId", Code: src.ManaUnitID, Reason: "not found in CHIS organization dictionary"}) } return errs } func resolveMasterData(src *PHISHealthRecord, lookup MasterDataLookup) { if lookup == nil { return } if src.RegionCode == "" && src.RegionName != "" { if code, ok := lookup.RegionCodeByName(src.RegionName); ok { src.RegionCode = code } } if src.ManaDoctorID == "" && src.ManaDoctorName != "" { if code, ok := lookup.DoctorIDByName(src.ManaDoctorName); ok { src.ManaDoctorID = code } } if src.ManaUnitID == "" && src.ManaUnitName != "" { if code, ok := lookup.ManaUnitIDByName(src.ManaUnitName); ok { src.ManaUnitID = code } } } func mapCode(dict Dict, code string, errs *[]ValidationError) string { if code == "" { return "" } mapped, err := dict.PHISToOSI(code) if err != nil { if validationErr, ok := err.(ValidationError); ok { *errs = append(*errs, validationErr) return "" } *errs = append(*errs, ValidationError{Field: dict.Field, Code: code, Reason: err.Error()}) return "" } return mapped } func mapDelimitedCode(dict Dict, value string, errs *[]ValidationError) string { parts := splitCodes(value) mapped := make([]string, 0, len(parts)) for _, code := range parts { mappedCode := mapCode(dict, code, errs) if mappedCode != "" { mapped = append(mapped, mappedCode) } } return strings.Join(mapped, ",") } func BuildHealthRecordCreate(mapped MappedHealthRecord) contract.HealthRecordCreate { return contract.HealthRecordCreate{ BaseInfo: contract.HealthRecordBaseInfo{ IDCard: mapped.IDCard, PersonName: mapped.PersonName, }, ManageInfo: contract.ManageInfo{OperateUser: mapped.ManaDoctorID}, HealthRecord: contract.HealthRecordCreateInfo{ CheckID: mapped.CheckID, IDCard: mapped.IDCard, PersonName: mapped.PersonName, SexCode: mapped.SexCode, Birthday: mapped.Birthday, MobileNumber: mapped.MobileNumber, WorkPlace: mapped.WorkPlace, Contact: mapped.Contact, ContactPhone: mapped.ContactPhone, RegisteredPermanent: mapped.RegisteredPermanent, RegionCode: mapped.RegionCode, Address: mapped.Address, AddressNumber: mapped.AddressNumber, HomePlace: mapped.HomePlace, HomePlaceNumber: mapped.HomePlaceNumber, CardType: mapped.CardType, ManaDoctorID: mapped.ManaDoctorID, ManaUnitID: mapped.ManaUnitID, CreateUser: mapped.ManaDoctorID, CreateUnit: mapped.ManaUnitID, NationCode: mapped.NationCode, EducationCode: mapped.EducationCode, WorkCode: mapped.WorkCode, MaritalStatusCode: mapped.MaritalStatusCode, BloodTypeCode: mapped.BloodTypeCode, RhBloodCode: mapped.RhBloodCode, InsuranceCode: mapped.InsuranceCode, IsFillSHHJ: mapped.IsFillSHHJ, CreateDate: mapped.CreateDate, }, PastHistory: mapped.PastHistory, PastDiseases: mapped.PastDiseases, PastSurgeries: mapped.PastSurgeries, PastTraumas: mapped.PastTraumas, PastTransfusions: mapped.PastTransfusions, FamilyMiddle: mapped.FamilyMiddle, } } func validateTaskFields(task source.HealthRecordTask) []ValidationError { r := task.Record var errs []ValidationError for _, field := range []struct{ name, value string }{ {"archId", task.ArchID}, {"contact", r.Contact}, {"contactPhone", r.ContactPhone}, {"registeredPermanent", r.RegisteredPermanent}, {"cardType", firstNonEmpty(r.CardType, task.CardType)}, {"nationCode", r.NationCode}, {"bloodTypeCode", r.BloodTypeCode}, {"rhBloodCode", r.RhBloodCode}, {"educationCode", r.EducationCode}, {"workCode", r.WorkCode}, {"maritalStatusCode", r.MaritalStatusCode}, {"insuranceCode", r.InsuranceCode}, {"ywgms", r.DrugAllergy}, {"cjqk", r.Disability}, } { if strings.TrimSpace(field.value) == "" { errs = append(errs, ValidationError{Field: field.name, Reason: "required"}) } } for _, field := range []struct { name, value string max int }{ {"idCard", r.IDCard, 18}, {"personName", r.PersonName, 50}, {"workPlace", r.WorkPlace, 70}, {"mobileNumber", r.MobileNumber, 20}, {"contact", r.Contact, 50}, {"contactPhone", r.ContactPhone, 20}, {"address", r.Address, 100}, {"addressNumber", r.AddressNumber, 70}, {"homePlace", r.HomePlace, 100}, {"homePlaceNumber", r.HomePlaceNumber, 70}, {"manaDoctorId", r.ManaDoctorID, 20}, {"manaUnitId", r.ManaUnitID, 20}, } { if utf8.RuneCountInString(field.value) > field.max { errs = append(errs, ValidationError{Field: field.name, Reason: "max length " + strconv.Itoa(field.max)}) } } for _, field := range []struct { name, value string length int }{ {"idCard", r.IDCard, 18}, {"regionCode", r.RegionCode, 12}, } { if utf8.RuneCountInString(field.value) != field.length { errs = append(errs, ValidationError{Field: field.name, Reason: "length " + strconv.Itoa(field.length)}) } } if _, err := time.Parse("2006-01-02", r.Birthday); err != nil { errs = append(errs, ValidationError{Field: "birthday", Reason: "invalid date, want yyyy-MM-dd"}) } if r.RegisteredPermanent != "1" && r.RegisteredPermanent != "2" { errs = append(errs, ValidationError{Field: "registeredPermanent", Code: r.RegisteredPermanent, Reason: "unknown code"}) } fill := strings.ToLower(strings.TrimSpace(r.IsFillSHHJ)) if fill != "y" && fill != "n" { errs = append(errs, ValidationError{Field: "isFillShhj", Code: r.IsFillSHHJ, Reason: "unknown code"}) } if task.Doctor.DoctorID != "" && task.Doctor.DoctorID != r.ManaDoctorID { errs = append(errs, ValidationError{Field: "manaDoctorId", Reason: "does not match data.doctor.doctorId"}) } return errs } func firstNonEmpty(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" { return strings.TrimSpace(value) } } return "" } func splitCodes(value string) []string { replacer := strings.NewReplacer(",", ",", "、", ",", ";", ",", ";", ",") var out []string for _, part := range strings.Split(replacer.Replace(value), ",") { if code := strings.TrimSpace(part); code != "" { out = append(out, code) } } return out } func validateDelimitedCodes(field, value string, allowed map[string]struct{}, errs *[]ValidationError) string { parts := splitCodes(value) valid := make([]string, 0, len(parts)) for _, code := range parts { if _, ok := allowed[code]; !ok { *errs = append(*errs, ValidationError{Field: field, Code: code, Reason: "unknown code"}) continue } valid = append(valid, code) } return strings.Join(valid, ",") } func validateSingleCode(field, value string, allowed map[string]struct{}, errs *[]ValidationError) string { if parts := splitCodes(value); len(parts) != 1 { *errs = append(*errs, ValidationError{Field: field, Reason: "must contain exactly one code"}) return "" } return validateDelimitedCodes(field, value, allowed, errs) } func codeSet(values ...string) map[string]struct{} { set := make(map[string]struct{}, len(values)) for _, value := range values { set[value] = struct{}{} } return set } var ( allergyCodes = codeSet("0101", "0102", "0103", "0104", "0109") exposureCodes = codeSet("1201", "1202", "1203", "1204") fatherCodes = numberedCodes("07") motherCodes = numberedCodes("08") siblingCodes = numberedCodes("09") childrenCodes = numberedCodes("10") geneticCodes = codeSet("0501", "0502") disabilityCodes = codeSet("1101", "1102", "1103", "1104", "1105", "1106", "1107", "1108", "1109", "1199") diseaseCodes = codeSet("0201", "0202", "0203", "0204", "0205", "0206", "0207", "0208", "0209", "0210", "0211", "0212", "0213", "0214", "0215", "0298", "0299") surgeryCodes = codeSet("0301", "0302") traumaCodes = codeSet("0601", "0602") transfusionCodes = codeSet("0401", "0402") cookAirCodes = codeSet("1", "2", "3", "4", "9") fuelCodes = codeSet("1", "2", "3", "4", "5", "9") waterCodes = codeSet("1", "2", "3", "4", "5", "9") washroomCodes = codeSet("1", "2", "3", "4", "5", "6") livestockCodes = codeSet("0", "1", "2", "3") ) func numberedCodes(prefix string) map[string]struct{} { values := []string{prefix + "01", prefix + "02", prefix + "03", prefix + "04", prefix + "05", prefix + "06", prefix + "07", prefix + "08", prefix + "09", prefix + "10", prefix + "11", prefix + "99"} return codeSet(values...) } func mapPastDiseases(value string) []contract.PastDisease { parts := splitCodes(value) out := make([]contract.PastDisease, 0, len(parts)) for _, code := range parts { out = append(out, contract.PastDisease{Code: code}) } return out } func mapPastSurgeries(value string) []contract.PastSurgery { parts := splitCodes(value) out := make([]contract.PastSurgery, 0, len(parts)) for _, code := range parts { out = append(out, contract.PastSurgery{Code: code}) } return out } func mapPastTraumas(value string) []contract.PastTrauma { parts := splitCodes(value) out := make([]contract.PastTrauma, 0, len(parts)) for _, code := range parts { out = append(out, contract.PastTrauma{Code: code}) } return out } func mapPastTransfusions(value string) []contract.PastBloodTx { parts := splitCodes(value) out := make([]contract.PastBloodTx, 0, len(parts)) for _, code := range parts { out = append(out, contract.PastBloodTx{Code: code}) } return out }