2025-10-17 17:32:44 -03:00

429 lines
12 KiB
Go

package core
import (
"context"
"encoding/json"
"fmt"
"ips-lacpass-backend/internal/ips/client"
customErrors "ips-lacpass-backend/pkg/errors"
authMiddleware "ips-lacpass-backend/pkg/middleware"
"log/slog"
"slices"
"sort"
"strings"
"time"
"github.com/google/uuid"
"github.com/mitchellh/mapstructure"
)
type IpsService struct {
Repository *client.IpsClient
}
func NewService(r *client.IpsClient) IpsService {
return IpsService{
Repository: r,
}
}
func (is *IpsService) GetIps(ctx context.Context) (map[string]interface{}, error) {
userId, err := authMiddleware.GetUserDocIDFromContext(ctx)
if err != nil {
return nil, &customErrors.HttpError{
StatusCode: 401,
Body: []map[string]interface{}{{"error": "user_identifier_not_found", "message": "User identifier not found in request context"}},
Err: err,
}
}
bundle, err := is.Repository.GetDocumentReference(userId)
if err != nil {
fmt.Printf("Error fetching document reference: %v\n", err)
return nil, err
}
entries := bundle.Entry
if len(entries) == 0 {
return nil, &customErrors.HttpError{
StatusCode: 404,
Body: []map[string]interface{}{{"error": "not_found", "message": "No IPS found for the user"}},
Err: fmt.Errorf("no IPS found for the user"),
}
}
sort.Slice(entries, func(i, j int) bool {
return entries[i].Resource.Meta.LastUpdated > entries[j].Resource.Meta.LastUpdated
})
ipsBundle, err := is.Repository.GetIpsBundle(entries[0].Resource.Content[0].Attachment.URL)
if err != nil {
return nil, err
}
return ipsBundle, nil
}
func (is *IpsService) GetIpsICVP(ctx context.Context, idBundle string, immunizationId *string) (string, error) {
result, err := is.Repository.GetIpsICVP(idBundle, immunizationId)
if err != nil {
return "", err
}
return result, nil
}
// Will return the IPS composition sections
func getIPSComposition(entries []Entry) (*Composition, error) {
i := slices.IndexFunc(entries, func(e Entry) bool {
if e.Resource["resourceType"] != "Composition" {
return false
}
var composition Composition
if err := mapstructure.Decode(e.Resource, &composition); err != nil {
return false
}
return composition.Type.Coding[0]["code"] == "60591-5"
})
if i == -1 {
return nil, fmt.Errorf(`no composition found`)
}
comp := entries[i].Resource
var composition Composition
if err := mapstructure.Decode(comp, &composition); err != nil {
return nil, fmt.Errorf(`error decoding composition: %v`, err)
}
composition.URL = entries[i].FullURL
// Remove empty sections
var result []Section
for _, s := range composition.Section {
if s.Code.Coding != nil {
result = append(result, s)
}
}
composition.Section = result
return &composition, nil
}
func getEntry(reference string, current []Entry, newIpsEntries []Entry) *Entry {
indInCurrent := slices.IndexFunc(current, func(e Entry) bool {
return e.FullURL == reference
})
if indInCurrent != -1 {
return &current[indInCurrent]
}
indInNew := slices.IndexFunc(newIpsEntries, func(e Entry) bool {
return e.FullURL == reference
})
if indInNew != -1 {
return &newIpsEntries[indInNew]
}
return nil
}
func findAllKeysContainingString(m map[string]interface{}, substring string) []string {
var matchingKeys []string
slower := strings.ToLower(substring)
for key := range m {
if strings.Contains(strings.ToLower(key), slower) {
matchingKeys = append(matchingKeys, key)
}
}
return matchingKeys
}
func removeDuplicates(entries []Entry) []Entry {
encountered := map[string]bool{}
var result []Entry
for _, e := range entries {
if !encountered[e.FullURL] {
encountered[e.FullURL] = true
result = append(result, e)
}
}
return result
}
func findCountryInBundle(bundle Bundle) string {
for _, entry := range bundle.Entry {
if entry.Resource == nil {
slog.Warn("No resource found in bundle: ", entry.FullURL)
continue
}
rtype, ok := entry.Resource["resourceType"]
if !ok || rtype == nil || rtype != "Organization" {
slog.Warn("Resource type is not an Organization: ", entry.FullURL)
continue
}
addressEntry, ok := entry.Resource["address"]
if !ok || addressEntry == nil {
slog.Warn("Resource has has noo Address: ", entry.FullURL)
continue
}
addresses, ok := entry.Resource["address"].([]interface{})
if !ok || len(addresses) == 0 {
slog.Warn("Resource has no address: ", entry.FullURL)
continue
}
address, ok := addresses[0].(map[string]interface{})
if !ok || address == nil {
slog.Warn("Resource address is not a map: ", entry.FullURL)
continue
}
country, ok := address["country"]
if !ok || country == nil {
slog.Warn("Resource address has no country: ", entry.FullURL)
continue
}
countryCode, ok := country.(string)
if !ok {
slog.Warn("Resource country is not a string: ", entry.FullURL)
continue
}
return countryCode
}
return ""
}
func findOrganizationEntries(bundle Bundle) []Entry {
var entries []Entry
for _, entry := range bundle.Entry {
if entry.Resource == nil {
slog.Warn("No resource found in bundle: ", entry.FullURL)
continue
}
rtype, ok := entry.Resource["resourceType"]
if !ok || rtype == nil || rtype != "Organization" {
continue
}
entries = append(entries, entry)
}
return entries
}
func addOriginExtension(resource map[string]interface{}, bundleID string, country string) {
if resource == nil {
return
}
originExtension := map[string]interface{}{
"url": "http://lacpass.org/fhir/StructureDefinition/resource-origin",
"extension": []map[string]interface{}{
{
"url": "bundleId",
"valueString": bundleID,
},
{
"url": "country",
"valueString": country,
},
},
}
extensions, ok := resource["extension"].([]interface{})
if !ok {
extensions = []interface{}{}
}
// Avoid adding duplicate extensions
for _, ext := range extensions {
extMap, ok := ext.(map[string]interface{})
if !ok {
continue
}
url, ok := extMap["url"].(string)
if !ok || url != "http://lacpass.org/fhir/StructureDefinition/resource-origin" {
continue
}
return
}
extensions = append(extensions, originExtension)
resource["extension"] = extensions
}
func (is *IpsService) MergeIPS(ctx context.Context, currentIpsBundle map[string]interface{}, newIpsBundle map[string]interface{}) (map[string]interface{}, error) {
var currIPS, newIPS Bundle
if err := mapstructure.Decode(currentIpsBundle, &currIPS); err != nil {
return nil, &customErrors.HttpError{
StatusCode: 400,
Body: []map[string]interface{}{{"error": "bad_request", "message": "Malformed current IPS"}},
Err: fmt.Errorf("malformed current IPS"),
}
}
if err := mapstructure.Decode(newIpsBundle, &newIPS); err != nil {
return nil, &customErrors.HttpError{
StatusCode: 400,
Body: []map[string]interface{}{{"error": "bad_request", "message": "Malformed new IPS"}},
Err: fmt.Errorf("malformed new IPS"),
}
}
currentCountry := findCountryInBundle(currIPS)
newCountry := findCountryInBundle(newIPS)
for i := range currIPS.Entry {
addOriginExtension(currIPS.Entry[i].Resource, currIPS.ID, currentCountry)
}
for i := range newIPS.Entry {
addOriginExtension(newIPS.Entry[i].Resource, newIPS.ID, newCountry)
}
curComp, err := getIPSComposition(currIPS.Entry)
if err != nil {
return nil, &customErrors.HttpError{
StatusCode: 400,
Body: []map[string]interface{}{{"error": "bad_request", "message": "Current IPS does not have its composition"}},
Err: err,
}
}
newComp, err := getIPSComposition(newIPS.Entry)
if err != nil {
return nil, &customErrors.HttpError{
StatusCode: 400,
Body: []map[string]interface{}{{"error": "bad_request", "message": "Current IPS does not have its composition"}},
Err: err,
}
}
// Merge composition for IPSs
mergedComp := curComp
for _, section := range newComp.Section {
code := section.Code.Coding[0]["code"]
if code == nil {
continue
}
sectionIndex := slices.IndexFunc(mergedComp.Section, func(s Section) bool {
return len(s.Code.Coding) > 0 && s.Code.Coding[0] != nil && s.Code.Coding[0]["code"] == code
})
if sectionIndex == -1 {
// New IPS section is not present on current IPS
mergedComp.Section = append(mergedComp.Section, section)
} else {
// Sections exists, add entries that do not exist in the current IPS
for _, newEntry := range section.Entry {
exists := false
for _, oldEntry := range mergedComp.Section[sectionIndex].Entry {
if newEntry["reference"] == oldEntry["reference"] {
exists = true
break
}
}
if !exists {
mergedComp.Section[sectionIndex].Entry = append(mergedComp.Section[sectionIndex].Entry, newEntry)
}
}
}
}
fullURL := mergedComp.URL
mergedComp.URL = ""
jsonData, err := json.Marshal(mergedComp)
if err != nil {
return nil, &customErrors.HttpError{
StatusCode: 500,
Body: []map[string]interface{}{{"error": "internal_error", "message": "Failed to convert composition to JSON"}},
Err: err,
}
}
var mergedResource map[string]interface{}
if err := json.Unmarshal(jsonData, &mergedResource); err != nil {
return nil, &customErrors.HttpError{
StatusCode: 500,
Body: []map[string]interface{}{{"error": "internal_error", "message": "Failed to convert composition JSON to a map"}},
Err: err,
}
}
// Build the merge ips with the merge Composition
mergedIPS := Bundle{
ID: uuid.NewString(),
Identifier: currIPS.Identifier,
Meta: currIPS.Meta,
ResourceType: currIPS.ResourceType,
Signature: nil,
Timestamp: time.Now().UTC().String(),
Type: currIPS.Type,
Entry: []Entry{{FullURL: fullURL, Resource: mergedResource}},
}
for _, section := range mergedComp.Section {
for _, secEntry := range section.Entry {
newEntry := getEntry(secEntry["reference"].(string), currIPS.Entry, newIPS.Entry)
if newEntry == nil {
break
}
mergedIPS.Entry = append(mergedIPS.Entry, *newEntry)
if newEntry.Resource == nil {
break
}
// Check for any resource that contains more reference in its representation
// If we find any reference we added it to the IPS
rk := findAllKeysContainingString(newEntry.Resource, "reference")
for _, k := range rk {
v, ok := newEntry.Resource[k]
if !ok {
break
}
var ref Reference
if err := mapstructure.Decode(v, &ref); err != nil {
return nil, fmt.Errorf(`error decoding codeable reference: %v`, err)
}
if ref.Reference != "" {
newEntry = getEntry(ref.Reference, currIPS.Entry, newIPS.Entry)
mergedIPS.Entry = append(mergedIPS.Entry, *newEntry)
}
}
}
}
// Add original Organization resources from both IPSs
currIPSOrganizations := findOrganizationEntries(currIPS)
newIPSOrganizations := findOrganizationEntries(newIPS)
for _, org := range newIPSOrganizations {
mergedIPS.Entry = append(mergedIPS.Entry, org)
}
for _, org := range currIPSOrganizations {
mergedIPS.Entry = append(mergedIPS.Entry, org)
}
mergedIPS.Entry = removeDuplicates(mergedIPS.Entry)
jsonData, err = json.Marshal(mergedIPS)
if err != nil {
return nil, &customErrors.HttpError{
StatusCode: 500,
Body: []map[string]interface{}{{"error": "internal_error", "message": "Failed to convert composition to JSON"}},
Err: err,
}
}
var data map[string]interface{}
if err := json.Unmarshal(jsonData, &data); err != nil {
return nil, &customErrors.HttpError{
StatusCode: 500,
Body: []map[string]interface{}{{"error": "internal_error", "message": "Failed to convert composition JSON to a map"}},
Err: err,
}
}
return data, nil
}