930 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			930 lines
		
	
	
		
			26 KiB
		
	
	
	
		
			Go
		
	
	
	
package application
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"context"
 | 
						|
	"encoding/json"
 | 
						|
	"errors"
 | 
						|
	"fmt"
 | 
						|
	"image/png"
 | 
						|
	"io"
 | 
						|
	"net/http"
 | 
						|
	"sort"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators"
 | 
						|
	"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
 | 
						|
	profilepictures "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/profile-pictures"
 | 
						|
	"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
 | 
						|
	filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
 | 
						|
	agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
 | 
						|
	agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
 | 
						|
	"git.coopgo.io/coopgo-platform/carpool-service/servers/grpc/proto"
 | 
						|
	fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
 | 
						|
	fleetsstorage "git.coopgo.io/coopgo-platform/fleets/storage"
 | 
						|
	groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
 | 
						|
	"git.coopgo.io/coopgo-platform/groups-management/storage"
 | 
						|
	mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
 | 
						|
	mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
 | 
						|
	"git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
 | 
						|
	solidaritytransformers "git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/transformers"
 | 
						|
	solidaritytypes "git.coopgo.io/coopgo-platform/solidarity-transport/types"
 | 
						|
	"github.com/google/uuid"
 | 
						|
	"github.com/rs/zerolog/log"
 | 
						|
	"google.golang.org/protobuf/types/known/structpb"
 | 
						|
	"google.golang.org/protobuf/types/known/timestamppb"
 | 
						|
)
 | 
						|
 | 
						|
type BeneficiariesResult struct {
 | 
						|
	Accounts []mobilityaccountsstorage.Account
 | 
						|
	CacheID  string
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) GetBeneficiaries(ctx context.Context, searchFilter string, archivedFilter bool) (*BeneficiariesResult, error) {
 | 
						|
	accounts, err := h.getBeneficiariesWithFilters(ctx, searchFilter, archivedFilter)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	sort.Sort(sorting.BeneficiariesByName(accounts))
 | 
						|
 | 
						|
	cacheID := uuid.NewString()
 | 
						|
	h.cache.PutWithTTL(cacheID, accounts, 1*time.Hour)
 | 
						|
 | 
						|
	return &BeneficiariesResult{
 | 
						|
		Accounts: accounts,
 | 
						|
		CacheID:  cacheID,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) CreateBeneficiary(ctx context.Context, firstName, lastName, email string, birthdate *time.Time, phoneNumber, fileNumber string, address any, gender string, otherProperties any) (string, error) {
 | 
						|
	// Get current group
 | 
						|
	g := ctx.Value(identification.GroupKey)
 | 
						|
	if g == nil {
 | 
						|
		return "", fmt.Errorf("no group found in context")
 | 
						|
	}
 | 
						|
	group := g.(storage.Group)
 | 
						|
 | 
						|
	// Create data map for the beneficiary
 | 
						|
	dataMap := map[string]any{
 | 
						|
		"first_name":   firstName,
 | 
						|
		"last_name":    lastName,
 | 
						|
		"email":        email,
 | 
						|
		"phone_number": phoneNumber,
 | 
						|
		"file_number":  fileNumber,
 | 
						|
		"gender":       gender,
 | 
						|
	}
 | 
						|
 | 
						|
	// Convert birthdate to string format for structpb compatibility
 | 
						|
	if birthdate != nil {
 | 
						|
		dataMap["birthdate"] = birthdate.Format("2006-01-02")
 | 
						|
	}
 | 
						|
 | 
						|
	if address != nil {
 | 
						|
		dataMap["address"] = address
 | 
						|
	}
 | 
						|
	if otherProperties != nil {
 | 
						|
		dataMap["other_properties"] = otherProperties
 | 
						|
	}
 | 
						|
 | 
						|
	data, err := structpb.NewValue(dataMap)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	request := &mobilityaccounts.RegisterRequest{
 | 
						|
		Account: &mobilityaccounts.Account{
 | 
						|
			Namespace: "parcoursmob_beneficiaries",
 | 
						|
			Data:      data.GetStructValue(),
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	resp, err := h.services.GRPC.MobilityAccounts.Register(ctx, request)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	subscribe := &groupsmanagement.SubscribeRequest{
 | 
						|
		Groupid:  group.ID,
 | 
						|
		Memberid: resp.Account.Id,
 | 
						|
	}
 | 
						|
 | 
						|
	_, err = h.services.GRPC.GroupsManagement.Subscribe(ctx, subscribe)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	return resp.Account.Id, nil
 | 
						|
}
 | 
						|
 | 
						|
type BeneficiaryDataResult struct {
 | 
						|
	Account                     mobilityaccountsstorage.Account
 | 
						|
	Bookings                    []fleetsstorage.Booking
 | 
						|
	Organizations               []any
 | 
						|
	Documents                   []filestorage.FileInfo
 | 
						|
	EventsList                  []Event_Beneficiary
 | 
						|
	SolidarityTransportStats    map[string]int64
 | 
						|
	SolidarityTransportBookings []*solidaritytypes.Booking
 | 
						|
	SolidarityDriversMap        map[string]mobilityaccountsstorage.Account
 | 
						|
	OrganizedCarpoolStats       map[string]int64
 | 
						|
	OrganizedCarpoolBookings    []*proto.CarpoolServiceBooking
 | 
						|
	OrganizedCarpoolDriversMap  map[string]mobilityaccountsstorage.Account
 | 
						|
	WalletBalance               float64
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) GetBeneficiaryData(ctx context.Context, beneficiaryID string) (*BeneficiaryDataResult, error) {
 | 
						|
	// Get beneficiary account
 | 
						|
	request := &mobilityaccounts.GetAccountRequest{
 | 
						|
		Id: beneficiaryID,
 | 
						|
	}
 | 
						|
 | 
						|
	resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// Security check: ensure this is actually a beneficiary account
 | 
						|
	if resp.Account.Namespace != "parcoursmob_beneficiaries" {
 | 
						|
		return nil, fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, resp.Account.Namespace)
 | 
						|
	}
 | 
						|
 | 
						|
	account := resp.Account.ToStorageType()
 | 
						|
 | 
						|
	// Get documents
 | 
						|
	documents := h.filestorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + beneficiaryID)
 | 
						|
 | 
						|
	// Get events subscriptions
 | 
						|
	subscriptionRequest := &agenda.GetSubscriptionByUserRequest{
 | 
						|
		Subscriber: beneficiaryID,
 | 
						|
	}
 | 
						|
 | 
						|
	subscriptionResp, err := h.services.GRPC.Agenda.GetSubscriptionByUser(ctx, subscriptionRequest)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	events := []agendastorage.Event{}
 | 
						|
	currentTime := time.Now().Truncate(24 * time.Hour)
 | 
						|
 | 
						|
	for _, e := range subscriptionResp.Subscription {
 | 
						|
		eventRequest := &agenda.GetEventRequest{
 | 
						|
			Id: e.Eventid,
 | 
						|
		}
 | 
						|
		eventResp, err := h.services.GRPC.Agenda.GetEvent(ctx, eventRequest)
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		events = append(events, eventResp.Event.ToStorageType())
 | 
						|
	}
 | 
						|
 | 
						|
	sort.Sort(sorting.EventsByStartdate(events))
 | 
						|
 | 
						|
	// Get bookings
 | 
						|
	bookingsRequest := &fleets.GetDriverBookingsRequest{
 | 
						|
		Driver: beneficiaryID,
 | 
						|
	}
 | 
						|
	bookingsResp, err := h.services.GRPC.Fleets.GetDriverBookings(ctx, bookingsRequest)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	bookings := []fleetsstorage.Booking{}
 | 
						|
	for _, b := range bookingsResp.Bookings {
 | 
						|
		bookings = append(bookings, b.ToStorageType())
 | 
						|
	}
 | 
						|
 | 
						|
	// Build events list
 | 
						|
	var eventsList []Event_Beneficiary
 | 
						|
	var statusEvent int
 | 
						|
 | 
						|
	for _, e := range events {
 | 
						|
		if e.Startdate.After(currentTime) {
 | 
						|
			statusEvent = 1
 | 
						|
		} else if e.Startdate.Before(currentTime) && e.Enddate.After(currentTime) || e.Enddate.Equal(currentTime) {
 | 
						|
			statusEvent = 2
 | 
						|
		} else {
 | 
						|
			statusEvent = 3
 | 
						|
		}
 | 
						|
 | 
						|
		event := Event{
 | 
						|
			NameVal:    e.Name,
 | 
						|
			DateVal:    e.Startdate,
 | 
						|
			DateEndVal: e.Enddate,
 | 
						|
			TypeVal:    e.Type,
 | 
						|
			IDVal:      e.ID,
 | 
						|
			DbVal:      "/app/agenda/",
 | 
						|
			IconSet:    "calendar",
 | 
						|
			StatusVal:  statusEvent,
 | 
						|
		}
 | 
						|
 | 
						|
		eventsList = append(eventsList, event)
 | 
						|
	}
 | 
						|
 | 
						|
	// Add vehicle bookings to events list
 | 
						|
	var statusBooking int
 | 
						|
	for _, b := range bookings {
 | 
						|
		if b.Enddate.After(currentTime) || b.Enddate.Equal(currentTime) {
 | 
						|
			getVehicleRequest := &fleets.GetVehicleRequest{
 | 
						|
				Vehicleid: b.Vehicleid,
 | 
						|
			}
 | 
						|
 | 
						|
			getVehicleResp, err := h.services.GRPC.Fleets.GetVehicle(ctx, getVehicleRequest)
 | 
						|
			if err != nil {
 | 
						|
				return nil, err
 | 
						|
			}
 | 
						|
 | 
						|
			if b.Startdate.After(currentTime) {
 | 
						|
				statusBooking = 1
 | 
						|
			} else if b.Startdate.Before(currentTime) && b.Enddate.After(currentTime) || b.Enddate.Equal(currentTime) {
 | 
						|
				statusBooking = 2
 | 
						|
			} else {
 | 
						|
				statusBooking = 3
 | 
						|
			}
 | 
						|
 | 
						|
			event := Event{
 | 
						|
				NameVal:    getVehicleResp.Vehicle.ToStorageType().Data["name"].(string),
 | 
						|
				DateVal:    b.Startdate,
 | 
						|
				DateEndVal: b.Enddate,
 | 
						|
				TypeVal:    "Réservation de véhicule",
 | 
						|
				IDVal:      b.ID,
 | 
						|
				DbVal:      "/app/vehicles-management/bookings/",
 | 
						|
				IconSet:    "vehicle",
 | 
						|
				StatusVal:  statusBooking,
 | 
						|
			}
 | 
						|
 | 
						|
			eventsList = append(eventsList, event)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Get solidarity transport bookings (all statuses for display)
 | 
						|
	solidarityResp, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBookings(ctx, &gen.GetSolidarityTransportBookingsRequest{
 | 
						|
		Passengerid: beneficiaryID,
 | 
						|
		StartDate:   timestamppb.New(time.Now().Add(-365 * 24 * time.Hour)),
 | 
						|
		EndDate:     timestamppb.New(time.Now().Add(365 * 24 * time.Hour)),
 | 
						|
	})
 | 
						|
 | 
						|
	protoBookings := []*gen.SolidarityTransportBooking{}
 | 
						|
	if err == nil {
 | 
						|
		protoBookings = solidarityResp.Bookings
 | 
						|
	} else {
 | 
						|
		log.Error().Err(err).Msg("error retrieving solidarity transport bookings for beneficiary")
 | 
						|
	}
 | 
						|
 | 
						|
	// Convert proto bookings to types with geojson.Feature
 | 
						|
	solidarityTransportBookings := []*solidaritytypes.Booking{}
 | 
						|
	for _, protoBooking := range protoBookings {
 | 
						|
		booking, err := solidaritytransformers.BookingProtoToType(protoBooking)
 | 
						|
		if err != nil {
 | 
						|
			log.Error().Err(err).Msg("error converting booking proto to type")
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		solidarityTransportBookings = append(solidarityTransportBookings, booking)
 | 
						|
	}
 | 
						|
 | 
						|
	// Don't filter out replaced bookings from beneficiary profile - show all bookings
 | 
						|
 | 
						|
	// Collect unique driver IDs
 | 
						|
	driverIDs := []string{}
 | 
						|
	driverIDsMap := make(map[string]bool)
 | 
						|
	for _, booking := range solidarityTransportBookings {
 | 
						|
		if booking.DriverId != "" {
 | 
						|
			if !driverIDsMap[booking.DriverId] {
 | 
						|
				driverIDs = append(driverIDs, booking.DriverId)
 | 
						|
				driverIDsMap[booking.DriverId] = true
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Get drivers in batch
 | 
						|
	driversMap := make(map[string]mobilityaccountsstorage.Account)
 | 
						|
	if len(driverIDs) > 0 {
 | 
						|
		driversResp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, &mobilityaccounts.GetAccountsBatchRequest{
 | 
						|
			Accountids: driverIDs,
 | 
						|
		})
 | 
						|
		if err == nil {
 | 
						|
			for _, account := range driversResp.Accounts {
 | 
						|
				a := account.ToStorageType()
 | 
						|
				driversMap[a.ID] = a
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Calculate stats only for validated bookings
 | 
						|
	solidarityTransportStats := map[string]int64{
 | 
						|
		"count": 0,
 | 
						|
		"km":    0,
 | 
						|
	}
 | 
						|
 | 
						|
	for _, b := range solidarityTransportBookings {
 | 
						|
		if b.Status == "VALIDATED" {
 | 
						|
			solidarityTransportStats["count"] = solidarityTransportStats["count"] + 1
 | 
						|
			if b.Journey != nil {
 | 
						|
				solidarityTransportStats["km"] = solidarityTransportStats["km"] + b.Journey.PassengerDistance
 | 
						|
			}
 | 
						|
 | 
						|
			// Add to events list
 | 
						|
			event := Event{
 | 
						|
				NameVal:    fmt.Sprintf("%s (%d km)", b.Journey.PassengerDrop.Properties.MustString("label", ""), b.Journey.PassengerDistance),
 | 
						|
				DateVal:    b.Journey.PassengerPickupDate,
 | 
						|
				DateEndVal: b.Journey.PassengerPickupDate,
 | 
						|
				TypeVal:    "Transport solidaire",
 | 
						|
				IDVal:      b.Id,
 | 
						|
				DbVal:      "/app/solidarity-transport/bookings/",
 | 
						|
				IconSet:    "vehicle",
 | 
						|
				StatusVal:  1,
 | 
						|
			}
 | 
						|
 | 
						|
			eventsList = append(eventsList, event)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Get organized carpool bookings
 | 
						|
	carpoolBookingsResp, err := h.services.GRPC.CarpoolService.GetUserBookings(ctx, &proto.GetUserBookingsRequest{
 | 
						|
		UserId: beneficiaryID,
 | 
						|
	})
 | 
						|
 | 
						|
	organizedCarpoolBookings := []*proto.CarpoolServiceBooking{}
 | 
						|
	if err == nil {
 | 
						|
		organizedCarpoolBookings = carpoolBookingsResp.Bookings
 | 
						|
	} else {
 | 
						|
		log.Error().Err(err).Msg("error retrieving organized carpool bookings for beneficiary")
 | 
						|
	}
 | 
						|
 | 
						|
	// Collect unique driver IDs from organized carpool bookings
 | 
						|
	carpoolDriverIDs := []string{}
 | 
						|
	carpoolDriverIDsMap := make(map[string]bool)
 | 
						|
	for _, booking := range organizedCarpoolBookings {
 | 
						|
		if booking.Driver != nil && booking.Driver.Id != "" {
 | 
						|
			if !carpoolDriverIDsMap[booking.Driver.Id] {
 | 
						|
				carpoolDriverIDs = append(carpoolDriverIDs, booking.Driver.Id)
 | 
						|
				carpoolDriverIDsMap[booking.Driver.Id] = true
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Get organized carpool drivers in batch
 | 
						|
	organizedCarpoolDriversMap := make(map[string]mobilityaccountsstorage.Account)
 | 
						|
	if len(carpoolDriverIDs) > 0 {
 | 
						|
		carpoolDriversResp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, &mobilityaccounts.GetAccountsBatchRequest{
 | 
						|
			Accountids: carpoolDriverIDs,
 | 
						|
		})
 | 
						|
		if err == nil {
 | 
						|
			for _, account := range carpoolDriversResp.Accounts {
 | 
						|
				a := account.ToStorageType()
 | 
						|
				organizedCarpoolDriversMap[a.ID] = a
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Calculate organized carpool stats (only confirmed bookings)
 | 
						|
	organizedCarpoolStats := map[string]int64{
 | 
						|
		"count": 0,
 | 
						|
		"km":    0,
 | 
						|
	}
 | 
						|
 | 
						|
	for _, cb := range organizedCarpoolBookings {
 | 
						|
		if cb.Status == proto.CarpoolServiceBookingStatus_CONFIRMED {
 | 
						|
			organizedCarpoolStats["count"]++
 | 
						|
			if cb.Distance != nil {
 | 
						|
				organizedCarpoolStats["km"] += *cb.Distance
 | 
						|
			}
 | 
						|
 | 
						|
			// Build journey name from drop address and distance for events
 | 
						|
			journeyName := "Covoiturage"
 | 
						|
			if cb.PassengerDropAddress != nil {
 | 
						|
				if cb.Distance != nil {
 | 
						|
					journeyName = fmt.Sprintf("%s (%d km)", *cb.PassengerDropAddress, *cb.Distance)
 | 
						|
				} else {
 | 
						|
					journeyName = *cb.PassengerDropAddress
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			// Get departure date
 | 
						|
			departureDate := time.Now()
 | 
						|
			if cb.PassengerPickupDate != nil {
 | 
						|
				departureDate = cb.PassengerPickupDate.AsTime()
 | 
						|
			}
 | 
						|
 | 
						|
			event := Event{
 | 
						|
				NameVal:    journeyName,
 | 
						|
				DateVal:    departureDate,
 | 
						|
				DateEndVal: departureDate,
 | 
						|
				TypeVal:    "Covoiturage solidaire",
 | 
						|
				IDVal:      cb.Id,
 | 
						|
				DbVal:      "/app/organized-carpool/bookings/",
 | 
						|
				IconSet:    "vehicle",
 | 
						|
				StatusVal:  1,
 | 
						|
			}
 | 
						|
 | 
						|
			eventsList = append(eventsList, event)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	sortByDate(eventsList)
 | 
						|
 | 
						|
	// Get organizations
 | 
						|
	groupsRequest := &groupsmanagement.GetGroupsRequest{
 | 
						|
		Namespaces: []string{"parcoursmob_organizations"},
 | 
						|
		Member:     beneficiaryID,
 | 
						|
	}
 | 
						|
 | 
						|
	groupsResp, err := h.services.GRPC.GroupsManagement.GetGroups(ctx, groupsRequest)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	organizations := []any{}
 | 
						|
	for _, o := range groupsResp.Groups {
 | 
						|
		organizations = append(organizations, o.ToStorageType())
 | 
						|
	}
 | 
						|
 | 
						|
	// Calculate wallet balance
 | 
						|
	walletBalance := h.calculateWalletBalance(account)
 | 
						|
 | 
						|
	return &BeneficiaryDataResult{
 | 
						|
		Account:                     account,
 | 
						|
		Bookings:                    bookings,
 | 
						|
		Organizations:               organizations,
 | 
						|
		Documents:                   documents,
 | 
						|
		EventsList:                  eventsList,
 | 
						|
		SolidarityTransportStats:    solidarityTransportStats,
 | 
						|
		SolidarityTransportBookings: solidarityTransportBookings,
 | 
						|
		SolidarityDriversMap:        driversMap,
 | 
						|
		OrganizedCarpoolStats:       organizedCarpoolStats,
 | 
						|
		OrganizedCarpoolBookings:    organizedCarpoolBookings,
 | 
						|
		OrganizedCarpoolDriversMap:  organizedCarpoolDriversMap,
 | 
						|
		WalletBalance:               walletBalance,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
type BeneficiaryResult struct {
 | 
						|
	Account mobilityaccountsstorage.Account
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) GetBeneficiary(ctx context.Context, beneficiaryID string) (*BeneficiaryResult, error) {
 | 
						|
	request := &mobilityaccounts.GetAccountRequest{
 | 
						|
		Id: beneficiaryID,
 | 
						|
	}
 | 
						|
 | 
						|
	resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// Security check: ensure this is actually a beneficiary account
 | 
						|
	if resp.Account.Namespace != "parcoursmob_beneficiaries" {
 | 
						|
		return nil, fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, resp.Account.Namespace)
 | 
						|
	}
 | 
						|
 | 
						|
	return &BeneficiaryResult{
 | 
						|
		Account: resp.Account.ToStorageType(),
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) UpdateBeneficiary(ctx context.Context, beneficiaryID, firstName, lastName, email string, birthdate *time.Time, phoneNumber, fileNumber string, address any, gender string, otherProperties any) (string, error) {
 | 
						|
	// Security check: verify the account exists and is a beneficiary
 | 
						|
	getRequest := &mobilityaccounts.GetAccountRequest{
 | 
						|
		Id: beneficiaryID,
 | 
						|
	}
 | 
						|
	getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
	if getResp.Account.Namespace != "parcoursmob_beneficiaries" {
 | 
						|
		return "", fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, getResp.Account.Namespace)
 | 
						|
	}
 | 
						|
 | 
						|
	// Create data map for the beneficiary
 | 
						|
	dataMap := map[string]any{
 | 
						|
		"first_name":   firstName,
 | 
						|
		"last_name":    lastName,
 | 
						|
		"email":        email,
 | 
						|
		"phone_number": phoneNumber,
 | 
						|
		"file_number":  fileNumber,
 | 
						|
		"gender":       gender,
 | 
						|
	}
 | 
						|
 | 
						|
	// Handle birthdate conversion for protobuf compatibility
 | 
						|
	if birthdate != nil {
 | 
						|
		dataMap["birthdate"] = birthdate.Format(time.RFC3339)
 | 
						|
	}
 | 
						|
 | 
						|
	if address != nil {
 | 
						|
		dataMap["address"] = address
 | 
						|
	}
 | 
						|
	if otherProperties != nil {
 | 
						|
		dataMap["other_properties"] = otherProperties
 | 
						|
	}
 | 
						|
 | 
						|
	data, err := structpb.NewValue(dataMap)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	request := &mobilityaccounts.UpdateDataRequest{
 | 
						|
		Account: &mobilityaccounts.Account{
 | 
						|
			Id:        beneficiaryID,
 | 
						|
			Namespace: "parcoursmob_beneficiaries",
 | 
						|
			Data:      data.GetStructValue(),
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	resp, err := h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
 | 
						|
	if err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	return resp.Account.Id, nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) ArchiveBeneficiary(ctx context.Context, beneficiaryID string) error {
 | 
						|
	// Security check: verify the account exists and is a beneficiary
 | 
						|
	getRequest := &mobilityaccounts.GetAccountRequest{
 | 
						|
		Id: beneficiaryID,
 | 
						|
	}
 | 
						|
	getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	if getResp.Account.Namespace != "parcoursmob_beneficiaries" {
 | 
						|
		return fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, getResp.Account.Namespace)
 | 
						|
	}
 | 
						|
 | 
						|
	data, err := structpb.NewValue(map[string]any{
 | 
						|
		"archived": true,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	request := &mobilityaccounts.UpdateDataRequest{
 | 
						|
		Account: &mobilityaccounts.Account{
 | 
						|
			Id:        beneficiaryID,
 | 
						|
			Namespace: "parcoursmob_beneficiaries",
 | 
						|
			Data:      data.GetStructValue(),
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	_, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) UnarchiveBeneficiary(ctx context.Context, beneficiaryID string) error {
 | 
						|
	// Security check: verify the account exists and is a beneficiary
 | 
						|
	getRequest := &mobilityaccounts.GetAccountRequest{
 | 
						|
		Id: beneficiaryID,
 | 
						|
	}
 | 
						|
	getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	if getResp.Account.Namespace != "parcoursmob_beneficiaries" {
 | 
						|
		return fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, getResp.Account.Namespace)
 | 
						|
	}
 | 
						|
 | 
						|
	data, err := structpb.NewValue(map[string]any{
 | 
						|
		"archived": false,
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	request := &mobilityaccounts.UpdateDataRequest{
 | 
						|
		Account: &mobilityaccounts.Account{
 | 
						|
			Id:        beneficiaryID,
 | 
						|
			Namespace: "parcoursmob_beneficiaries",
 | 
						|
			Data:      data.GetStructValue(),
 | 
						|
		},
 | 
						|
	}
 | 
						|
 | 
						|
	_, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
 | 
						|
	return err
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) GetBeneficiaryPicture(ctx context.Context, beneficiaryID string) ([]byte, string, error) {
 | 
						|
	request := &mobilityaccounts.GetAccountRequest{
 | 
						|
		Id: beneficiaryID,
 | 
						|
	}
 | 
						|
 | 
						|
	resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
 | 
						|
	if err != nil {
 | 
						|
		return nil, "", err
 | 
						|
	}
 | 
						|
 | 
						|
	// Security check: ensure this is actually a beneficiary account
 | 
						|
	// if resp.Account.Namespace != "parcoursmob_beneficiaries" {
 | 
						|
	// 	return nil, "", fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, resp.Account.Namespace)
 | 
						|
	// }
 | 
						|
 | 
						|
	account := resp.Account.ToStorageType()
 | 
						|
 | 
						|
	firstName, ok := account.Data["first_name"].(string)
 | 
						|
	if !ok || firstName == "" {
 | 
						|
		firstName = "U"
 | 
						|
	}
 | 
						|
	lastName, ok := account.Data["last_name"].(string)
 | 
						|
	if !ok || lastName == "" {
 | 
						|
		lastName = "U"
 | 
						|
	}
 | 
						|
 | 
						|
	initials := strings.ToUpper(string(firstName[0]) + string(lastName[0]))
 | 
						|
	picture := profilepictures.DefaultProfilePicture(initials)
 | 
						|
 | 
						|
	buffer := new(bytes.Buffer)
 | 
						|
	if err := png.Encode(buffer, picture); err != nil {
 | 
						|
		return nil, "", err
 | 
						|
	}
 | 
						|
 | 
						|
	return buffer.Bytes(), "image/png", nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) AddBeneficiaryDocument(ctx context.Context, beneficiaryID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error {
 | 
						|
	// Security check: verify the account exists and is a beneficiary
 | 
						|
	getRequest := &mobilityaccounts.GetAccountRequest{
 | 
						|
		Id: beneficiaryID,
 | 
						|
	}
 | 
						|
	getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	if getResp.Account.Namespace != "parcoursmob_beneficiaries" {
 | 
						|
		return fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, getResp.Account.Namespace)
 | 
						|
	}
 | 
						|
 | 
						|
	fileid := uuid.NewString()
 | 
						|
 | 
						|
	metadata := map[string]string{
 | 
						|
		"type": documentType,
 | 
						|
		"name": documentName,
 | 
						|
	}
 | 
						|
 | 
						|
	if err := h.filestorage.Put(file, filestorage.PREFIX_BENEFICIARIES, fmt.Sprintf("%s/%s_%s", beneficiaryID, fileid, filename), fileSize, metadata); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) GetBeneficiaryDocument(ctx context.Context, beneficiaryID, document string) (io.Reader, *filestorage.FileInfo, error) {
 | 
						|
	// Security check: verify the account exists and is a beneficiary
 | 
						|
	getRequest := &mobilityaccounts.GetAccountRequest{
 | 
						|
		Id: beneficiaryID,
 | 
						|
	}
 | 
						|
	getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
 | 
						|
	if err != nil {
 | 
						|
		return nil, nil, err
 | 
						|
	}
 | 
						|
	if getResp.Account.Namespace != "parcoursmob_beneficiaries" {
 | 
						|
		return nil, nil, fmt.Errorf("account %s is not a beneficiary (namespace: %s)", beneficiaryID, getResp.Account.Namespace)
 | 
						|
	}
 | 
						|
 | 
						|
	file, info, err := h.filestorage.Get(filestorage.PREFIX_BENEFICIARIES, fmt.Sprintf("%s/%s", beneficiaryID, document))
 | 
						|
	if err != nil {
 | 
						|
		return nil, nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return file, info, nil
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) DeleteBeneficiaryDocument(ctx context.Context, beneficiaryID, document string) error {
 | 
						|
	return h.DeleteDocument(ctx, BeneficiaryDocumentConfig, beneficiaryID, document)
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) getBeneficiariesWithFilters(ctx context.Context, searchFilter string, archivedFilter bool) ([]mobilityaccountsstorage.Account, error) {
 | 
						|
	accounts := []mobilityaccountsstorage.Account{}
 | 
						|
	g := ctx.Value(identification.GroupKey)
 | 
						|
	if g == nil {
 | 
						|
		return accounts, errors.New("no group provided")
 | 
						|
	}
 | 
						|
 | 
						|
	group := g.(storage.Group)
 | 
						|
 | 
						|
	request := &mobilityaccounts.GetAccountsBatchRequest{
 | 
						|
		Accountids: group.Members,
 | 
						|
	}
 | 
						|
 | 
						|
	resp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, request)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Msg("issue in mobilityaccounts call")
 | 
						|
		return accounts, err
 | 
						|
	}
 | 
						|
 | 
						|
	for _, account := range resp.Accounts {
 | 
						|
		if h.filterAccount(account, searchFilter, archivedFilter) {
 | 
						|
			a := account.ToStorageType()
 | 
						|
			accounts = append(accounts, a)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return accounts, err
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) filterAccount(a *mobilityaccounts.Account, searchFilter string, archivedFilter bool) bool {
 | 
						|
	// Search filter
 | 
						|
	if searchFilter != "" {
 | 
						|
		name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string)
 | 
						|
		if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter)) {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Archived filter
 | 
						|
	if archivedFilter {
 | 
						|
		if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
		return false
 | 
						|
	} else {
 | 
						|
		if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
type Event_Beneficiary interface {
 | 
						|
	Name() string
 | 
						|
	Date() time.Time
 | 
						|
	DateEnd() time.Time
 | 
						|
	Type() string
 | 
						|
	Db() string
 | 
						|
	ID() string
 | 
						|
	Icons() string
 | 
						|
	Status() int
 | 
						|
}
 | 
						|
 | 
						|
type Event struct {
 | 
						|
	IDVal      string
 | 
						|
	NameVal    string
 | 
						|
	DateVal    time.Time
 | 
						|
	DateEndVal time.Time
 | 
						|
	TypeVal    string
 | 
						|
	DbVal      string
 | 
						|
	Deleted    bool
 | 
						|
	IconSet    string
 | 
						|
	StatusVal  int
 | 
						|
}
 | 
						|
 | 
						|
func (e Event) Name() string {
 | 
						|
	return e.NameVal
 | 
						|
}
 | 
						|
 | 
						|
func (e Event) Date() time.Time {
 | 
						|
	return e.DateVal
 | 
						|
}
 | 
						|
 | 
						|
func (e Event) DateEnd() time.Time {
 | 
						|
	return e.DateEndVal
 | 
						|
}
 | 
						|
 | 
						|
func (e Event) Type() string {
 | 
						|
	return e.TypeVal
 | 
						|
}
 | 
						|
 | 
						|
func (e Event) ID() string {
 | 
						|
	return e.IDVal
 | 
						|
}
 | 
						|
 | 
						|
func (e Event) Db() string {
 | 
						|
	return e.DbVal
 | 
						|
}
 | 
						|
 | 
						|
func (e Event) Icons() string {
 | 
						|
	return e.IconSet
 | 
						|
}
 | 
						|
 | 
						|
func (e Event) Status() int {
 | 
						|
	return e.StatusVal
 | 
						|
}
 | 
						|
 | 
						|
func sortByDate(events []Event_Beneficiary) {
 | 
						|
	sort.Slice(events, func(i, j int) bool {
 | 
						|
		return events[i].Date().After(events[j].Date())
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
// Utility functions needed by other modules
 | 
						|
func filterAccount(r *http.Request, a *mobilityaccounts.Account) bool {
 | 
						|
	searchFilter, ok := r.URL.Query()["search"]
 | 
						|
 | 
						|
	if ok && len(searchFilter[0]) > 0 {
 | 
						|
		name := a.Data.AsMap()["first_name"].(string) + " " + a.Data.AsMap()["last_name"].(string)
 | 
						|
		if !strings.Contains(strings.ToLower(name), strings.ToLower(searchFilter[0])) {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	archivedFilter, ok := r.URL.Query()["archived"]
 | 
						|
	if ok && archivedFilter[0] == "true" {
 | 
						|
		if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
 | 
						|
			return true
 | 
						|
		}
 | 
						|
		return false
 | 
						|
	} else {
 | 
						|
		if archived, ok := a.Data.AsMap()["archived"].(bool); ok && archived {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return true
 | 
						|
}
 | 
						|
 | 
						|
func (h *ApplicationHandler) beneficiaries(r *http.Request) ([]mobilityaccountsstorage.Account, error) {
 | 
						|
	accounts := []mobilityaccountsstorage.Account{}
 | 
						|
	g := r.Context().Value(identification.GroupKey)
 | 
						|
	if g == nil {
 | 
						|
		return accounts, errors.New("no group provided")
 | 
						|
	}
 | 
						|
 | 
						|
	group := g.(storage.Group)
 | 
						|
 | 
						|
	request := &mobilityaccounts.GetAccountsBatchRequest{
 | 
						|
		Accountids: group.Members,
 | 
						|
	}
 | 
						|
 | 
						|
	resp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), request)
 | 
						|
	if err != nil {
 | 
						|
		log.Error().Err(err).Msg("issue in mobilityaccounts call")
 | 
						|
		return accounts, err
 | 
						|
	}
 | 
						|
 | 
						|
	for _, account := range resp.Accounts {
 | 
						|
		if filterAccount(r, account) {
 | 
						|
			a := account.ToStorageType()
 | 
						|
			accounts = append(accounts, a)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return accounts, err
 | 
						|
}
 | 
						|
 | 
						|
type BeneficiariesForm struct {
 | 
						|
	FirstName       string     `json:"first_name" validate:"required"`
 | 
						|
	LastName        string     `json:"last_name" validate:"required"`
 | 
						|
	Email           string     `json:"email" validate:"required,email"`
 | 
						|
	Birthdate       *time.Time `json:"birthdate" validate:"required"`
 | 
						|
	PhoneNumber     string     `json:"phone_number" validate:"required,phoneNumber"`
 | 
						|
	FileNumber      string     `json:"file_number"`
 | 
						|
	Address         any        `json:"address,omitempty"`
 | 
						|
	Gender          string     `json:"gender"`
 | 
						|
	OtherProperties any        `json:"other_properties,omitempty"`
 | 
						|
}
 | 
						|
 | 
						|
func parseBeneficiariesForm(r *http.Request) (map[string]any, error) {
 | 
						|
	if err := r.ParseForm(); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	var date *time.Time
 | 
						|
 | 
						|
	if r.PostFormValue("birthdate") != "" {
 | 
						|
		d, err := time.Parse("2006-01-02", r.PostFormValue("birthdate"))
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		date = &d
 | 
						|
	}
 | 
						|
 | 
						|
	formData := BeneficiariesForm{
 | 
						|
		FirstName:   r.PostFormValue("first_name"),
 | 
						|
		LastName:    r.PostFormValue("last_name"),
 | 
						|
		Email:       r.PostFormValue("email"),
 | 
						|
		Birthdate:   date,
 | 
						|
		PhoneNumber: r.PostFormValue("phone_number"),
 | 
						|
		FileNumber:  r.PostFormValue("file_number"),
 | 
						|
		Gender:      r.PostFormValue("gender"),
 | 
						|
	}
 | 
						|
 | 
						|
	if r.PostFormValue("address") != "" {
 | 
						|
		var a any
 | 
						|
		json.Unmarshal([]byte(r.PostFormValue("address")), &a)
 | 
						|
		formData.Address = a
 | 
						|
	}
 | 
						|
 | 
						|
	if r.PostFormValue("other_properties") != "" {
 | 
						|
		var a any
 | 
						|
		json.Unmarshal([]byte(r.PostFormValue("other_properties")), &a)
 | 
						|
		formData.OtherProperties = a
 | 
						|
	}
 | 
						|
 | 
						|
	validate := formvalidators.New()
 | 
						|
	if err := validate.Struct(formData); err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	d, err := json.Marshal(formData)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	var dataMap map[string]any
 | 
						|
	err = json.Unmarshal(d, &dataMap)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return dataMap, nil
 | 
						|
}
 |