lot of new functionalities
This commit is contained in:
658
core/application/administration.go
Executable file
658
core/application/administration.go
Executable file
@@ -0,0 +1,658 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
|
||||
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
|
||||
agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
|
||||
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"
|
||||
groupstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
|
||||
accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
|
||||
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
|
||||
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
type AdministrationDataResult struct {
|
||||
Accounts []mobilityaccountsstorage.Account
|
||||
Beneficiaries []mobilityaccountsstorage.Account
|
||||
Groups []groupstorage.Group
|
||||
Bookings []fleetsstorage.Booking
|
||||
Events []agendastorage.Event
|
||||
}
|
||||
|
||||
type AdminVehiclesStatsResult struct {
|
||||
Vehicles []fleetsstorage.Vehicle
|
||||
Bookings []fleetsstorage.Booking
|
||||
Groups map[string]any
|
||||
}
|
||||
|
||||
type AdminBookingsStatsResult struct {
|
||||
Vehicles map[string]fleetsstorage.Vehicle
|
||||
Bookings []fleetsstorage.Booking
|
||||
Groups map[string]any
|
||||
BeneficiariesMap map[string]any
|
||||
}
|
||||
|
||||
type AdminBeneficiariesStatsResult struct {
|
||||
Beneficiaries []mobilityaccountsstorage.Account
|
||||
CacheID string
|
||||
}
|
||||
|
||||
type AdminEventsStatsResult struct {
|
||||
Events []agendastorage.Event
|
||||
Groups map[string]any
|
||||
}
|
||||
|
||||
// GetAdministrationData retrieves all data needed for the administration dashboard
|
||||
func (h *ApplicationHandler) GetAdministrationData(ctx context.Context) (*AdministrationDataResult, error) {
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
accounts, beneficiaries []mobilityaccountsstorage.Account
|
||||
bookings []fleetsstorage.Booking
|
||||
accountsErr, beneficiariesErr, bookingsErr, groupsResponseErr, eventsResponseErr, groupsBatchErr error
|
||||
groups = []groupstorage.Group{}
|
||||
responses = []agendastorage.Event{}
|
||||
groupsResponse *groupsmanagement.GetGroupsResponse
|
||||
eventsResponse *agenda.GetEventsResponse
|
||||
groupids = []string{}
|
||||
)
|
||||
|
||||
// Retrieve accounts in a goroutine
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
accounts, accountsErr = h.services.GetAccounts()
|
||||
}()
|
||||
|
||||
// Retrieve beneficiaries in a goroutine
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
beneficiaries, beneficiariesErr = h.services.GetBeneficiaries()
|
||||
}()
|
||||
|
||||
// Retrieve bookings in a goroutine
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
bookings, bookingsErr = h.services.GetBookings()
|
||||
}()
|
||||
|
||||
// Retrieve groups in a goroutine
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
request := &groupsmanagement.GetGroupsRequest{
|
||||
Namespaces: []string{"parcoursmob_organizations"},
|
||||
}
|
||||
groupsResponse, groupsResponseErr = h.services.GRPC.GroupsManagement.GetGroups(ctx, request)
|
||||
if groupsResponseErr == nil {
|
||||
for _, group := range groupsResponse.Groups {
|
||||
g := group.ToStorageType()
|
||||
groups = append(groups, g)
|
||||
}
|
||||
sort.Sort(sorting.GroupsByName(groups))
|
||||
}
|
||||
}()
|
||||
|
||||
// Retrieve Events in a goroutine
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
eventsResponse, eventsResponseErr = h.services.GRPC.Agenda.GetEvents(ctx, &agenda.GetEventsRequest{
|
||||
Namespaces: []string{"parcoursmob_dispositifs"},
|
||||
})
|
||||
if eventsResponseErr == nil {
|
||||
for _, e := range eventsResponse.Events {
|
||||
groupids = append(groupids, e.Owners...)
|
||||
responses = append(responses, e.ToStorageType())
|
||||
}
|
||||
sort.Sort(sorting.EventsByStartdate(responses))
|
||||
}
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Check for errors
|
||||
if accountsErr != nil || beneficiariesErr != nil || bookingsErr != nil || groupsResponseErr != nil || eventsResponseErr != nil {
|
||||
log.Error().
|
||||
Any("accounts error", accountsErr).
|
||||
Any("beneficiaries error", beneficiariesErr).
|
||||
Any("bookings error", bookingsErr).
|
||||
Any("groups response error", groupsResponseErr).
|
||||
Any("events response error", eventsResponseErr).
|
||||
Any("groups batch error", groupsBatchErr).
|
||||
Msg("Error in retrieving administration data")
|
||||
return nil, fmt.Errorf("error retrieving administration data")
|
||||
}
|
||||
|
||||
return &AdministrationDataResult{
|
||||
Accounts: accounts,
|
||||
Beneficiaries: beneficiaries,
|
||||
Groups: groups,
|
||||
Bookings: bookings,
|
||||
Events: responses,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateAdministrationGroup creates a new administration group
|
||||
func (h *ApplicationHandler) CreateAdministrationGroup(ctx context.Context, name string, modules map[string]any) (string, error) {
|
||||
groupid := uuid.NewString()
|
||||
|
||||
dataMap := map[string]any{
|
||||
"name": name,
|
||||
"modules": modules,
|
||||
}
|
||||
|
||||
data, err := structpb.NewValue(dataMap)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot create PB struct from data map")
|
||||
return "", fmt.Errorf("failed to create group data: %w", err)
|
||||
}
|
||||
|
||||
request_organization := &groupsmanagement.AddGroupRequest{
|
||||
Group: &groupsmanagement.Group{
|
||||
Id: groupid,
|
||||
Namespace: "parcoursmob_organizations",
|
||||
Data: data.GetStructValue(),
|
||||
},
|
||||
}
|
||||
|
||||
request_role := &groupsmanagement.AddGroupRequest{
|
||||
Group: &groupsmanagement.Group{
|
||||
Id: groupid + ":admin",
|
||||
Namespace: "parcoursmob_roles",
|
||||
},
|
||||
}
|
||||
|
||||
// Create organization group
|
||||
_, err = h.services.GRPC.GroupsManagement.AddGroup(ctx, request_organization)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Issue in Groups management service - AddGroup")
|
||||
return "", fmt.Errorf("failed to create organization group: %w", err)
|
||||
}
|
||||
|
||||
// Create admin role for the organization
|
||||
_, err = h.services.GRPC.GroupsManagement.AddGroup(ctx, request_role)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Issue in Groups management service - AddGroup")
|
||||
return "", fmt.Errorf("failed to create admin role: %w", err)
|
||||
}
|
||||
|
||||
return groupid, nil
|
||||
}
|
||||
|
||||
type AdministrationGroupDataResult struct {
|
||||
Group groupstorage.Group
|
||||
Members []mobilityaccountsstorage.Account
|
||||
Admins []mobilityaccountsstorage.Account
|
||||
}
|
||||
|
||||
// GetAdministrationGroupData retrieves data for a specific administration group
|
||||
func (h *ApplicationHandler) GetAdministrationGroupData(ctx context.Context, groupID string) (*AdministrationGroupDataResult, error) {
|
||||
request := &groupsmanagement.GetGroupRequest{
|
||||
Id: groupID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, request)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Issue in Groups management service - GetGroup")
|
||||
return nil, fmt.Errorf("failed to get group: %w", err)
|
||||
}
|
||||
|
||||
groupmembers, admins, err := h.groupmembers(groupID)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("issue retrieving group members")
|
||||
return nil, fmt.Errorf("failed to get group members: %w", err)
|
||||
}
|
||||
|
||||
return &AdministrationGroupDataResult{
|
||||
Group: resp.Group.ToStorageType(),
|
||||
Members: groupmembers,
|
||||
Admins: admins,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// InviteAdministrationGroupAdmin invites a user as admin to an administration group
|
||||
func (h *ApplicationHandler) InviteAdministrationGroupAdmin(ctx context.Context, groupID, username string) error {
|
||||
// Get group info
|
||||
groupResp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, &groupsmanagement.GetGroupRequest{
|
||||
Id: groupID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get group: %w", err)
|
||||
}
|
||||
|
||||
group := groupResp.Group.ToStorageType()
|
||||
|
||||
accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(ctx, &mobilityaccounts.GetAccountUsernameRequest{
|
||||
Username: username,
|
||||
Namespace: "parcoursmob",
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
// Account already exists: adding the existing account to group as admin
|
||||
account := accountresp.Account.ToStorageType()
|
||||
if account.Data["groups"] == nil {
|
||||
account.Data["groups"] = []any{}
|
||||
}
|
||||
account.Data["groups"] = append(account.Data["groups"].([]any), groupID+":admin")
|
||||
|
||||
as, err := mobilityaccounts.AccountFromStorageType(&account)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert account: %w", err)
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, &mobilityaccounts.UpdateDataRequest{
|
||||
Account: as,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update account: %w", err)
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"group": group.Data["name"],
|
||||
"baseUrl": h.config.GetString("base_url"),
|
||||
}
|
||||
|
||||
if err := h.emailing.Send("onboarding.existing_administrator", username, data); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to send existing admin email")
|
||||
}
|
||||
} else {
|
||||
// Create onboarding for new admin
|
||||
onboarding := map[string]any{
|
||||
"username": username,
|
||||
"group": groupID,
|
||||
"admin": true,
|
||||
}
|
||||
|
||||
b := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return fmt.Errorf("failed to generate random key: %w", err)
|
||||
}
|
||||
key := base64.RawURLEncoding.EncodeToString(b)
|
||||
|
||||
h.cache.PutWithTTL("onboarding/"+key, onboarding, 72*time.Hour)
|
||||
|
||||
data := map[string]any{
|
||||
"group": group.Data["name"],
|
||||
"key": key,
|
||||
"baseUrl": h.config.GetString("base_url"),
|
||||
}
|
||||
|
||||
if err := h.emailing.Send("onboarding.new_administrator", username, data); err != nil {
|
||||
return fmt.Errorf("failed to send new admin email: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// InviteAdministrationGroupMember invites a user as member to an administration group
|
||||
func (h *ApplicationHandler) InviteAdministrationGroupMember(ctx context.Context, groupID, username string) error {
|
||||
// Get group info
|
||||
groupResp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, &groupsmanagement.GetGroupRequest{
|
||||
Id: groupID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get group: %w", err)
|
||||
}
|
||||
|
||||
group := groupResp.Group.ToStorageType()
|
||||
|
||||
accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(ctx, &mobilityaccounts.GetAccountUsernameRequest{
|
||||
Username: username,
|
||||
Namespace: "parcoursmob",
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
// Account already exists: adding the existing account to group
|
||||
account := accountresp.Account.ToStorageType()
|
||||
if account.Data["groups"] == nil {
|
||||
account.Data["groups"] = []any{}
|
||||
}
|
||||
account.Data["groups"] = append(account.Data["groups"].([]any), groupID)
|
||||
|
||||
as, err := mobilityaccounts.AccountFromStorageType(&account)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert account: %w", err)
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, &mobilityaccounts.UpdateDataRequest{
|
||||
Account: as,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update account: %w", err)
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"group": group.Data["name"],
|
||||
"baseUrl": h.config.GetString("base_url"),
|
||||
}
|
||||
|
||||
if err := h.emailing.Send("onboarding.existing_member", username, data); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to send existing member email")
|
||||
}
|
||||
} else {
|
||||
// Create onboarding for new member
|
||||
onboarding := map[string]any{
|
||||
"username": username,
|
||||
"group": groupID,
|
||||
"admin": false,
|
||||
}
|
||||
|
||||
b := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return fmt.Errorf("failed to generate random key: %w", err)
|
||||
}
|
||||
key := base64.RawURLEncoding.EncodeToString(b)
|
||||
|
||||
h.cache.PutWithTTL("onboarding/"+key, onboarding, 72*time.Hour)
|
||||
|
||||
data := map[string]any{
|
||||
"group": group.Data["name"],
|
||||
"key": key,
|
||||
"baseUrl": h.config.GetString("base_url"),
|
||||
}
|
||||
|
||||
if err := h.emailing.Send("onboarding.new_member", username, data); err != nil {
|
||||
return fmt.Errorf("failed to send new member email: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func filteVehicle(r *http.Request, v *fleets.Vehicle) bool {
|
||||
g := r.Context().Value(identification.GroupKey)
|
||||
if g == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
group := g.(storage.Group)
|
||||
|
||||
for _, n := range v.Administrators {
|
||||
if n == group.ID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetVehiclesStats() (*AdminVehiclesStatsResult, error) {
|
||||
bookings := []fleetsstorage.Booking{}
|
||||
administrators := []string{}
|
||||
request := &fleets.GetVehiclesRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
}
|
||||
resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vehicles := []fleetsstorage.Vehicle{}
|
||||
for _, vehicle := range resp.Vehicles {
|
||||
v := vehicle.ToStorageType()
|
||||
adminfound := false
|
||||
for _, a := range administrators {
|
||||
if len(v.Administrators) > 0 && a == v.Administrators[0] {
|
||||
adminfound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !adminfound && len(v.Administrators) > 0 {
|
||||
administrators = append(administrators, v.Administrators[0])
|
||||
}
|
||||
|
||||
vehicleBookings := []fleetsstorage.Booking{}
|
||||
for _, b := range v.Bookings {
|
||||
if b.Unavailableto.After(time.Now()) {
|
||||
vehicleBookings = append(vehicleBookings, b)
|
||||
}
|
||||
}
|
||||
|
||||
v.Bookings = vehicleBookings
|
||||
vehicles = append(vehicles, v)
|
||||
}
|
||||
|
||||
groups := map[string]any{}
|
||||
if len(administrators) > 0 {
|
||||
admingroups, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
|
||||
Groupids: administrators,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, g := range admingroups.Groups {
|
||||
groups[g.Id] = g.ToStorageType()
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sorting.VehiclesByLicencePlate(vehicles))
|
||||
sort.Sort(sorting.BookingsByStartdate(bookings))
|
||||
|
||||
return &AdminVehiclesStatsResult{
|
||||
Vehicles: vehicles,
|
||||
Bookings: bookings,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetBookingsStats(status, startDate, endDate string) (*AdminBookingsStatsResult, error) {
|
||||
vehicles := map[string]fleetsstorage.Vehicle{}
|
||||
bookings := []fleetsstorage.Booking{}
|
||||
|
||||
// Parse start date filter
|
||||
var startdate time.Time
|
||||
if startDate != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", startDate); err == nil {
|
||||
startdate = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Parse end date filter
|
||||
var enddate time.Time
|
||||
if endDate != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", endDate); err == nil {
|
||||
enddate = parsed.Add(24 * time.Hour) // End of day
|
||||
}
|
||||
}
|
||||
|
||||
request := &fleets.GetVehiclesRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
IncludeDeleted: true,
|
||||
}
|
||||
resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
beneficiaries_ids := []string{}
|
||||
|
||||
for _, vehicle := range resp.Vehicles {
|
||||
v := vehicle.ToStorageType()
|
||||
|
||||
for _, b := range v.Bookings {
|
||||
// Apply status filter
|
||||
if status != "" {
|
||||
bookingStatus := b.Status()
|
||||
statusInt := 0
|
||||
|
||||
if b.Deleted {
|
||||
statusInt = -2 // Use -2 for cancelled to distinguish from terminated
|
||||
} else {
|
||||
statusInt = bookingStatus
|
||||
}
|
||||
|
||||
// Map status string to int
|
||||
var filterStatusInt int
|
||||
switch status {
|
||||
case "FORTHCOMING":
|
||||
filterStatusInt = 1
|
||||
case "ONGOING":
|
||||
filterStatusInt = 0
|
||||
case "TERMINATED":
|
||||
filterStatusInt = -1
|
||||
case "CANCELLED":
|
||||
filterStatusInt = -2
|
||||
default:
|
||||
filterStatusInt = 999 // Invalid status, won't match anything
|
||||
}
|
||||
|
||||
if statusInt != filterStatusInt {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Apply date filter (on startdate)
|
||||
if !startdate.IsZero() && b.Startdate.Before(startdate) {
|
||||
continue
|
||||
}
|
||||
if !enddate.IsZero() && b.Startdate.After(enddate) {
|
||||
continue
|
||||
}
|
||||
|
||||
bookings = append(bookings, b)
|
||||
beneficiaries_ids = append(beneficiaries_ids, b.Driver)
|
||||
}
|
||||
|
||||
vehicles[v.ID] = v
|
||||
}
|
||||
|
||||
groups := map[string]any{}
|
||||
|
||||
admingroups, err := h.services.GRPC.GroupsManagement.GetGroups(context.TODO(), &groupsmanagement.GetGroupsRequest{
|
||||
Namespaces: []string{"parcoursmob_organizations"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, g := range admingroups.Groups {
|
||||
groups[g.Id] = g.ToStorageType()
|
||||
}
|
||||
|
||||
beneficiaries, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), &accounts.GetAccountsBatchRequest{
|
||||
Accountids: beneficiaries_ids,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
beneficiaries_map := map[string]any{}
|
||||
for _, ben := range beneficiaries.Accounts {
|
||||
beneficiaries_map[ben.Id] = ben.ToStorageType()
|
||||
}
|
||||
|
||||
return &AdminBookingsStatsResult{
|
||||
Vehicles: vehicles,
|
||||
Bookings: bookings,
|
||||
Groups: groups,
|
||||
BeneficiariesMap: beneficiaries_map,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetBeneficiariesStats() (*AdminBeneficiariesStatsResult, error) {
|
||||
beneficiaries, err := h.services.GetBeneficiaries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cacheid := uuid.New().String()
|
||||
h.cache.Put(cacheid, beneficiaries)
|
||||
|
||||
return &AdminBeneficiariesStatsResult{
|
||||
Beneficiaries: beneficiaries,
|
||||
CacheID: cacheid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetEventsStats() (*AdminEventsStatsResult, error) {
|
||||
resp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
|
||||
Namespaces: []string{"parcoursmob_dispositifs"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responses := []agendastorage.Event{}
|
||||
groupids := []string{}
|
||||
|
||||
for _, event := range resp.Events {
|
||||
responses = append(responses, event.ToStorageType())
|
||||
groupids = append(groupids, event.Owners...)
|
||||
}
|
||||
|
||||
groupsResponse, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
|
||||
Groupids: groupids,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groups := map[string]any{}
|
||||
for _, g := range groupsResponse.Groups {
|
||||
groups[g.Id] = g.ToStorageType()
|
||||
}
|
||||
|
||||
return &AdminEventsStatsResult{
|
||||
Events: responses,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) members() ([]*accounts.Account, error) {
|
||||
resp, err := h.services.GRPC.MobilityAccounts.GetAccounts(context.TODO(), &accounts.GetAccountsRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp.Accounts, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) groupmembers(groupid string) (groupmembers []mobilityaccountsstorage.Account, admins []mobilityaccountsstorage.Account, err error) {
|
||||
members, err := h.members()
|
||||
if err != nil {
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Cannot get members")
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
groupmembers = []mobilityaccountsstorage.Account{}
|
||||
admins = []mobilityaccountsstorage.Account{}
|
||||
|
||||
for _, m := range members {
|
||||
mm := m.ToStorageType()
|
||||
for _, g := range mm.Data["groups"].([]any) {
|
||||
if g.(string) == groupid {
|
||||
groupmembers = append(groupmembers, mm)
|
||||
}
|
||||
if g.(string) == groupid+":admin" {
|
||||
admins = append(admins, mm)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return groupmembers, admins, err
|
||||
}
|
||||
602
core/application/agenda.go
Executable file
602
core/application/agenda.go
Executable file
@@ -0,0 +1,602 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/services"
|
||||
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"
|
||||
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"
|
||||
ics "github.com/arran4/golang-ical"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
|
||||
type AgendaEventsResult struct {
|
||||
Events []agendastorage.Event
|
||||
Groups map[string]any
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetAgendaEvents(ctx context.Context, minDate, maxDate *time.Time) (*AgendaEventsResult, error) {
|
||||
request := &agenda.GetEventsRequest{
|
||||
Namespaces: []string{"parcoursmob_dispositifs"},
|
||||
}
|
||||
|
||||
if minDate != nil {
|
||||
request.Mindate = timestamppb.New(*minDate)
|
||||
}
|
||||
if maxDate != nil {
|
||||
request.Maxdate = timestamppb.New(*maxDate)
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Agenda.GetEvents(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responses := []agendastorage.Event{}
|
||||
groupids := []string{}
|
||||
for _, e := range resp.Events {
|
||||
groupids = append(groupids, e.Owners...)
|
||||
responses = append(responses, e.ToStorageType())
|
||||
}
|
||||
|
||||
sort.Sort(sorting.EventsByStartdate(responses))
|
||||
|
||||
groups := map[string]any{}
|
||||
if len(groupids) > 0 {
|
||||
groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(ctx, &groupsmanagement.GetGroupsBatchRequest{
|
||||
Groupids: groupids,
|
||||
})
|
||||
if err == nil {
|
||||
for _, g := range groupsresp.Groups {
|
||||
groups[g.Id] = g.ToStorageType()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &AgendaEventsResult{
|
||||
Events: responses,
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
func (h *ApplicationHandler) CreateAgendaEvent(ctx context.Context, name, eventType, description string, address any, allday bool, startdate, enddate *time.Time, starttime, endtime string, maxSubscribers int, file io.Reader, filename string, fileSize int64, documentType, documentName string) (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)
|
||||
|
||||
data, _ := structpb.NewStruct(map[string]any{
|
||||
"address": address,
|
||||
})
|
||||
|
||||
request := &agenda.CreateEventRequest{
|
||||
Event: &agenda.Event{
|
||||
Namespace: "parcoursmob_dispositifs",
|
||||
Owners: []string{group.ID},
|
||||
Type: eventType,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Startdate: timestamppb.New(*startdate),
|
||||
Enddate: timestamppb.New(*enddate),
|
||||
Starttime: starttime,
|
||||
Endtime: endtime,
|
||||
Allday: allday,
|
||||
MaxSubscribers: int64(maxSubscribers),
|
||||
Data: data,
|
||||
Deleted: false,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Agenda.CreateEvent(ctx, request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Handle file upload if provided
|
||||
if file != nil && filename != "" {
|
||||
fileid := uuid.NewString()
|
||||
|
||||
metadata := map[string]string{
|
||||
"file_type": documentType,
|
||||
"file_name": documentName,
|
||||
}
|
||||
|
||||
if err := h.filestorage.Put(file, filestorage.PREFIX_AGENDA, fmt.Sprintf("%s/%s_%s", resp.Event.Id, fileid, filename), fileSize, metadata); err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return resp.Event.Id, nil
|
||||
}
|
||||
|
||||
|
||||
type AgendaEventResult struct {
|
||||
Event agendastorage.Event
|
||||
Group storage.Group
|
||||
Documents []filestorage.FileInfo
|
||||
Subscribers map[string]any
|
||||
Accounts []any
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetAgendaEvent(ctx context.Context, eventID string) (*AgendaEventResult, error) {
|
||||
request := &agenda.GetEventRequest{
|
||||
Id: eventID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
grouprequest := &groupsmanagement.GetGroupRequest{
|
||||
Id: resp.Event.Owners[0],
|
||||
}
|
||||
|
||||
groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, grouprequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscribers := map[string]any{}
|
||||
accids := []string{}
|
||||
for _, v := range resp.Event.Subscriptions {
|
||||
accids = append(accids, v.Subscriber)
|
||||
}
|
||||
|
||||
if len(accids) > 0 {
|
||||
subscriberresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
|
||||
ctx,
|
||||
&mobilityaccounts.GetAccountsBatchRequest{
|
||||
Accountids: accids,
|
||||
},
|
||||
)
|
||||
if err == nil {
|
||||
for _, sub := range subscriberresp.Accounts {
|
||||
subscribers[sub.Id] = sub.ToStorageType()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g := ctx.Value(identification.GroupKey)
|
||||
if g == nil {
|
||||
return nil, fmt.Errorf("no group found in context")
|
||||
}
|
||||
|
||||
group := g.(storage.Group)
|
||||
|
||||
accountids := []string{}
|
||||
for _, m := range group.Members {
|
||||
if !contains(resp.Event.Subscriptions, m) {
|
||||
accountids = append(accountids, m)
|
||||
}
|
||||
}
|
||||
|
||||
accounts := []any{}
|
||||
if len(accountids) > 0 {
|
||||
accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
|
||||
ctx,
|
||||
&mobilityaccounts.GetAccountsBatchRequest{
|
||||
Accountids: accountids,
|
||||
},
|
||||
)
|
||||
if err == nil {
|
||||
for _, acc := range accountresp.Accounts {
|
||||
accounts = append(accounts, acc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
documents := h.filestorage.List(filestorage.PREFIX_AGENDA + "/" + eventID)
|
||||
|
||||
return &AgendaEventResult{
|
||||
Event: resp.Event.ToStorageType(),
|
||||
Group: groupresp.Group.ToStorageType(),
|
||||
Documents: documents,
|
||||
Subscribers: subscribers,
|
||||
Accounts: accounts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
func (h *ApplicationHandler) SubscribeToAgendaEvent(ctx context.Context, eventID, subscriber string, subscriptionData map[string]any) error {
|
||||
datapb, err := structpb.NewStruct(subscriptionData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := &agenda.SubscribeEventRequest{
|
||||
Eventid: eventID,
|
||||
Subscriber: subscriber,
|
||||
Data: datapb,
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.Agenda.SubscribeEvent(ctx, request)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) UnsubscribeFromAgendaEvent(ctx context.Context, eventID, subscribeID, motif, currentUserID, currentUserDisplayName, currentUserEmail, currentGroupID, currentGroupName string) error {
|
||||
// Get the event first
|
||||
request := &agenda.GetEventRequest{
|
||||
Id: eventID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Find subscription data for the subscriber being removed
|
||||
var s_b_id, s_b_name, s_b_email, s_b_group_id, s_b_group_name string
|
||||
for i := range resp.Event.Subscriptions {
|
||||
if resp.Event.Subscriptions[i].Subscriber == subscribeID {
|
||||
s_b_id = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["id"].GetStringValue()
|
||||
s_b_name = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["display_name"].GetStringValue()
|
||||
s_b_email = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["user"].GetStructValue().Fields["email"].GetStringValue()
|
||||
s_b_group_id = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["group"].GetStructValue().Fields["id"].GetStringValue()
|
||||
s_b_group_name = resp.Event.Subscriptions[i].Data.Fields["subscribed_by"].GetStructValue().Fields["group"].GetStructValue().Fields["name"].GetStringValue()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"subscribed_by": map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": s_b_id,
|
||||
"display_name": s_b_name,
|
||||
"email": s_b_email,
|
||||
},
|
||||
"group": map[string]any{
|
||||
"id": s_b_group_id,
|
||||
"name": s_b_group_name,
|
||||
},
|
||||
},
|
||||
"unsubscribed_by": map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": currentUserID,
|
||||
"display_name": currentUserDisplayName,
|
||||
"email": currentUserEmail,
|
||||
},
|
||||
"group": map[string]any{
|
||||
"id": currentGroupID,
|
||||
"name": currentGroupName,
|
||||
},
|
||||
},
|
||||
"motif": motif,
|
||||
}
|
||||
|
||||
datapb, err := structpb.NewStruct(data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteRequest := &agenda.DeleteSubscriptionRequest{
|
||||
Subscriber: subscribeID,
|
||||
Eventid: eventID,
|
||||
Data: datapb,
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.Agenda.DeleteSubscription(ctx, deleteRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Send email notification
|
||||
emailData := map[string]any{
|
||||
"motif": motif,
|
||||
"user": currentUserDisplayName,
|
||||
"subscriber": fmt.Sprintf("http://localhost:9000/app/beneficiaries/%s", subscribeID),
|
||||
"link": fmt.Sprintf("http://localhost:9000/app/agenda/%s", eventID),
|
||||
}
|
||||
|
||||
if err := h.emailing.Send("delete_subscriber.request", s_b_email, emailData); err != nil {
|
||||
log.Error().Err(err).Msg("Cannot send email")
|
||||
// Don't return error for email failure
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type AgendaEventHistoryResult struct {
|
||||
Event agendastorage.Event
|
||||
Group storage.Group
|
||||
Subscribers map[string]any
|
||||
Accounts []any
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetAgendaEventHistory(ctx context.Context, eventID string) (*AgendaEventHistoryResult, error) {
|
||||
request := &agenda.GetEventRequest{
|
||||
Id: eventID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
grouprequest := &groupsmanagement.GetGroupRequest{
|
||||
Id: resp.Event.Owners[0],
|
||||
}
|
||||
|
||||
groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, grouprequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subscribers := map[string]any{}
|
||||
|
||||
accids := []string{}
|
||||
for _, v := range resp.Event.DeletedSubscription {
|
||||
accids = append(accids, v.Subscriber)
|
||||
}
|
||||
|
||||
if len(accids) > 0 {
|
||||
subscriberresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
|
||||
ctx,
|
||||
&mobilityaccounts.GetAccountsBatchRequest{
|
||||
Accountids: accids,
|
||||
},
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
for _, sub := range subscriberresp.Accounts {
|
||||
subscribers[sub.Id] = sub.ToStorageType()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
g := ctx.Value(identification.GroupKey)
|
||||
if g == nil {
|
||||
return nil, fmt.Errorf("no group found in context")
|
||||
}
|
||||
|
||||
group := g.(storage.Group)
|
||||
|
||||
accountids := []string{}
|
||||
for _, m := range group.Members {
|
||||
if !contains(resp.Event.DeletedSubscription, m) {
|
||||
accountids = append(accountids, m)
|
||||
}
|
||||
}
|
||||
|
||||
accounts := []any{}
|
||||
if len(accountids) > 0 {
|
||||
accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(
|
||||
ctx,
|
||||
&mobilityaccounts.GetAccountsBatchRequest{
|
||||
Accountids: accountids,
|
||||
},
|
||||
)
|
||||
|
||||
if err == nil {
|
||||
for _, acc := range accountresp.Accounts {
|
||||
accounts = append(accounts, acc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &AgendaEventHistoryResult{
|
||||
Event: resp.Event.ToStorageType(),
|
||||
Group: groupresp.Group.ToStorageType(),
|
||||
Subscribers: subscribers,
|
||||
Accounts: accounts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) AddEventDocument(ctx context.Context, eventID string, file io.Reader, filename string, fileSize int64, documentType, documentName string) error {
|
||||
fileid := uuid.NewString()
|
||||
|
||||
metadata := map[string]string{
|
||||
"type": documentType,
|
||||
"name": documentName,
|
||||
}
|
||||
|
||||
if err := h.filestorage.Put(file, filestorage.PREFIX_AGENDA, fmt.Sprintf("%s/%s_%s", eventID, fileid, filename), fileSize, metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetEventDocument(ctx context.Context, eventID, document string) (io.Reader, *filestorage.FileInfo, error) {
|
||||
file, info, err := h.filestorage.Get(filestorage.PREFIX_AGENDA, fmt.Sprintf("%s/%s", eventID, document))
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return file, info, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
func contains(s []*agenda.Subscription, e string) bool {
|
||||
for _, a := range s {
|
||||
if a.Subscriber == e {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) UpdateAgendaEvent(ctx context.Context, eventID, name, eventType, description string, address any, allday bool, startdate, enddate *time.Time, starttime, endtime string, maxSubscribers int) (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)
|
||||
|
||||
// Get existing event first
|
||||
getRequest := &agenda.GetEventRequest{
|
||||
Id: eventID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Agenda.GetEvent(ctx, getRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, _ := structpb.NewStruct(map[string]any{
|
||||
"address": address,
|
||||
})
|
||||
|
||||
request := &agenda.UpdateEventRequest{
|
||||
Event: &agenda.Event{
|
||||
Namespace: "parcoursmob_dispositifs",
|
||||
Id: eventID,
|
||||
Owners: []string{group.ID},
|
||||
Type: eventType,
|
||||
Name: name,
|
||||
Description: description,
|
||||
Startdate: timestamppb.New(*startdate),
|
||||
Enddate: timestamppb.New(*enddate),
|
||||
Starttime: starttime,
|
||||
Endtime: endtime,
|
||||
Allday: allday,
|
||||
MaxSubscribers: int64(maxSubscribers),
|
||||
Data: data,
|
||||
Subscriptions: resp.Event.Subscriptions,
|
||||
},
|
||||
}
|
||||
|
||||
updateResp, err := h.services.GRPC.Agenda.UpdateEvent(ctx, request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return updateResp.Event.Id, nil
|
||||
}
|
||||
|
||||
|
||||
func (h *ApplicationHandler) DeleteAgendaEvent(ctx context.Context, eventID string) error {
|
||||
request := &agenda.GetEventRequest{
|
||||
Id: eventID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Agenda.GetEvent(ctx, request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
updateRequest := &agenda.UpdateEventRequest{
|
||||
Event: &agenda.Event{
|
||||
Namespace: resp.Event.Namespace,
|
||||
Id: resp.Event.Id,
|
||||
Owners: resp.Event.Owners,
|
||||
Type: resp.Event.Type,
|
||||
Name: resp.Event.Name,
|
||||
Description: resp.Event.Description,
|
||||
Startdate: resp.Event.Startdate,
|
||||
Enddate: resp.Event.Enddate,
|
||||
Starttime: resp.Event.Starttime,
|
||||
Endtime: resp.Event.Endtime,
|
||||
Allday: resp.Event.Allday,
|
||||
MaxSubscribers: int64(resp.Event.MaxSubscribers),
|
||||
Data: resp.Event.Data,
|
||||
Subscriptions: resp.Event.Subscriptions,
|
||||
Deleted: true,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.Agenda.UpdateEvent(ctx, updateRequest)
|
||||
return err
|
||||
}
|
||||
|
||||
type CalendarResult struct {
|
||||
CalendarData string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GenerateGlobalCalendar(ctx context.Context) (*CalendarResult, error) {
|
||||
events, err := h.services.GetAgendaEvents()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving agenda events")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
calendar, err := h.icsCalendar(events)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CalendarResult{
|
||||
CalendarData: calendar.Serialize(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GenerateOrganizationCalendar(ctx context.Context, groupID string) (*CalendarResult, error) {
|
||||
events, err := h.services.GetAgendaEvents()
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving agenda events")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filteredEvents := []services.AgendaEvent{}
|
||||
for _, e := range events {
|
||||
for _, g := range e.Owners {
|
||||
if g == groupID {
|
||||
filteredEvents = append(filteredEvents, e)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
calendar, err := h.icsCalendar(filteredEvents)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &CalendarResult{
|
||||
CalendarData: calendar.Serialize(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) icsCalendar(events []services.AgendaEvent) (*ics.Calendar, error) {
|
||||
calendar := ics.NewCalendarFor(h.config.GetString("service_name"))
|
||||
|
||||
for _, e := range events {
|
||||
vevent := ics.NewEvent(e.ID)
|
||||
vevent.SetSummary(e.Name)
|
||||
vevent.SetDescription(e.Description)
|
||||
if e.Allday {
|
||||
vevent.SetAllDayStartAt(e.Startdate)
|
||||
if e.Enddate.After(e.Startdate) {
|
||||
vevent.SetAllDayEndAt(e.Enddate.Add(24 * time.Hour))
|
||||
}
|
||||
} else {
|
||||
timeloc, err := time.LoadLocation("Europe/Paris")
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Tried to load timezone location Europe/Paris. Error. Missing zones in container ?")
|
||||
return nil, err
|
||||
}
|
||||
vevent.SetStartAt(e.Startdate.In(timeloc))
|
||||
vevent.SetEndAt(e.Enddate.In(timeloc))
|
||||
}
|
||||
calendar.AddVEvent(vevent)
|
||||
}
|
||||
|
||||
return calendar, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
67
core/application/application.go
Executable file
67
core/application/application.go
Executable file
@@ -0,0 +1,67 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/services"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
||||
cache "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
|
||||
"git.coopgo.io/coopgo-platform/emailing"
|
||||
"git.coopgo.io/coopgo-platform/groups-management/storage"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type ApplicationHandler struct {
|
||||
config *viper.Viper
|
||||
services *services.ServicesHandler
|
||||
cache cache.CacheHandler
|
||||
filestorage cache.FileStorage
|
||||
emailing *emailing.Mailer
|
||||
idp *identification.IdentificationProvider
|
||||
}
|
||||
|
||||
func NewApplicationHandler(cfg *viper.Viper, svc *services.ServicesHandler, cache cache.CacheHandler, filestorage cache.FileStorage, emailing *emailing.Mailer, idp *identification.IdentificationProvider) (*ApplicationHandler, error) {
|
||||
return &ApplicationHandler{
|
||||
config: cfg,
|
||||
services: svc,
|
||||
cache: cache,
|
||||
filestorage: filestorage,
|
||||
emailing: emailing,
|
||||
idp: idp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
func (h *ApplicationHandler) templateFile(file string) string {
|
||||
return h.config.GetString("templates.root") + file
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) currentGroup(r *http.Request) (current_group storage.Group, err error) {
|
||||
g := r.Context().Value(identification.GroupKey)
|
||||
if g == nil {
|
||||
return storage.Group{}, errors.New("current group not found")
|
||||
}
|
||||
current_group = g.(storage.Group)
|
||||
|
||||
return current_group, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) currentUser(r *http.Request) (current_user_token *oidc.IDToken, current_user_claims map[string]any, err error) {
|
||||
// Get current user ID
|
||||
u := r.Context().Value(identification.IdtokenKey)
|
||||
if u == nil {
|
||||
return nil, nil, errors.New("current user not found")
|
||||
}
|
||||
current_user_token = u.(*oidc.IDToken)
|
||||
|
||||
// Get current user claims
|
||||
c := r.Context().Value(identification.ClaimsKey)
|
||||
if c == nil {
|
||||
return current_user_token, nil, errors.New("current user claims not found")
|
||||
}
|
||||
current_user_claims = c.(map[string]any)
|
||||
|
||||
return current_user_token, current_user_claims, nil
|
||||
}
|
||||
240
core/application/auth.go
Normal file
240
core/application/auth.go
Normal file
@@ -0,0 +1,240 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-platform/groups-management/grpcapi"
|
||||
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
|
||||
ma "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
groupsstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type OAuth2CallbackResult struct {
|
||||
RedirectURL string
|
||||
IDToken string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) ProcessOAuth2Callback(code string, redirectSession string) (*OAuth2CallbackResult, error) {
|
||||
oauth2Token, err := h.idp.OAuth2Config.Exchange(context.Background(), code)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Exchange error")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Extract the ID Token from OAuth2 token.
|
||||
rawIDToken, ok := oauth2Token.Extra("id_token").(string)
|
||||
if !ok {
|
||||
log.Error().Msg("Cannot retrieve ID token")
|
||||
return nil, errors.New("cannot retrieve ID token")
|
||||
}
|
||||
|
||||
_, err = h.idp.TokenVerifier.Verify(context.Background(), rawIDToken)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Not able to verify token")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
redirect := "/app/"
|
||||
if redirectSession != "" {
|
||||
redirect = redirectSession
|
||||
}
|
||||
|
||||
return &OAuth2CallbackResult{
|
||||
RedirectURL: redirect,
|
||||
IDToken: rawIDToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type LostPasswordInitResult struct {
|
||||
Success bool
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) InitiateLostPassword(email string) (*LostPasswordInitResult, error) {
|
||||
account, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(context.TODO(), &mobilityaccounts.GetAccountUsernameRequest{
|
||||
Username: email,
|
||||
Namespace: "parcoursmob",
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
b := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
key := base64.RawURLEncoding.EncodeToString(b)
|
||||
|
||||
passwordretrieval := map[string]any{
|
||||
"username": email,
|
||||
"account_id": account.Account.Id,
|
||||
"key": key,
|
||||
}
|
||||
|
||||
h.cache.PutWithTTL("retrieve-password/"+key, passwordretrieval, 72*time.Hour)
|
||||
|
||||
if err := h.emailing.Send("auth.retrieve_password", email, passwordretrieval); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &LostPasswordInitResult{Success: true}, nil
|
||||
}
|
||||
|
||||
type LostPasswordRecoverResult struct {
|
||||
Success bool
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) RecoverLostPassword(key, newPassword string) (*LostPasswordRecoverResult, error) {
|
||||
recover, err := h.cache.Get("retrieve-password/" + key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if newPassword == "" {
|
||||
return nil, errors.New("password is empty")
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.MobilityAccounts.ChangePassword(context.TODO(), &mobilityaccounts.ChangePasswordRequest{
|
||||
Id: recover.(map[string]any)["account_id"].(string),
|
||||
Password: newPassword,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = h.cache.Delete("retrieve-password/" + key)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to delete password recovery key")
|
||||
}
|
||||
|
||||
return &LostPasswordRecoverResult{Success: true}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetPasswordRecoveryData(key string) (map[string]any, error) {
|
||||
recover, err := h.cache.Get("retrieve-password/" + key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return recover.(map[string]any), nil
|
||||
}
|
||||
|
||||
type OnboardingResult struct {
|
||||
Success bool
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) CompleteOnboarding(key, password, firstName, lastName string) (*OnboardingResult, error) {
|
||||
onboarding, err := h.cache.Get("onboarding/" + key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
onboardingmap := onboarding.(map[string]any)
|
||||
|
||||
if password == "" {
|
||||
return nil, errors.New("password is empty")
|
||||
}
|
||||
|
||||
groups := []string{
|
||||
onboardingmap["group"].(string),
|
||||
}
|
||||
|
||||
if onboardingmap["admin"].(bool) {
|
||||
groups = append(groups, onboardingmap["group"].(string)+":admin")
|
||||
}
|
||||
|
||||
display_name := firstName + " " + lastName
|
||||
account := &ma.Account{
|
||||
Authentication: ma.AccountAuth{
|
||||
Local: ma.LocalAuth{
|
||||
Username: onboardingmap["username"].(string),
|
||||
Password: password,
|
||||
},
|
||||
},
|
||||
Namespace: "parcoursmob",
|
||||
Data: map[string]any{
|
||||
"display_name": display_name,
|
||||
"first_name": firstName,
|
||||
"last_name": lastName,
|
||||
"email": onboardingmap["username"],
|
||||
"groups": groups,
|
||||
},
|
||||
}
|
||||
|
||||
acc, err := mobilityaccounts.AccountFromStorageType(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
request := &mobilityaccounts.RegisterRequest{
|
||||
Account: acc,
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.MobilityAccounts.Register(context.TODO(), request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = h.cache.Delete("onboarding/" + key)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("Failed to delete onboarding key")
|
||||
}
|
||||
|
||||
return &OnboardingResult{Success: true}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetOnboardingData(key string) (map[string]any, error) {
|
||||
onboarding, err := h.cache.Get("onboarding/" + key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return onboarding.(map[string]any), nil
|
||||
}
|
||||
|
||||
type UserGroupsResult struct {
|
||||
Groups []groupsstorage.Group
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetUserGroups(idtoken *oidc.IDToken) (*UserGroupsResult, error) {
|
||||
var claims map[string]any
|
||||
err := idtoken.Claims(&claims)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := claims["groups"]
|
||||
groups_interface, ok := g.([]any)
|
||||
if !ok {
|
||||
return nil, errors.New("invalid groups format")
|
||||
}
|
||||
|
||||
groups := []string{}
|
||||
for _, v := range groups_interface {
|
||||
groups = append(groups, v.(string))
|
||||
}
|
||||
|
||||
request := &grpcapi.GetGroupsBatchRequest{
|
||||
Groupids: groups,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groupsresponse []groupsstorage.Group
|
||||
for _, group := range resp.Groups {
|
||||
if group.Namespace != "parcoursmob_organizations" {
|
||||
continue
|
||||
}
|
||||
g := group.ToStorageType()
|
||||
groupsresponse = append(groupsresponse, g)
|
||||
}
|
||||
|
||||
return &UserGroupsResult{Groups: groupsresponse}, nil
|
||||
}
|
||||
927
core/application/beneficiaries.go
Normal file
927
core/application/beneficiaries.go
Normal file
@@ -0,0 +1,927 @@
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
228
core/application/dashboard.go
Executable file
228
core/application/dashboard.go
Executable file
@@ -0,0 +1,228 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
|
||||
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
|
||||
agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
|
||||
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
|
||||
fleetstorage "git.coopgo.io/coopgo-platform/fleets/storage"
|
||||
"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"
|
||||
"github.com/paulmach/orb"
|
||||
"github.com/paulmach/orb/geojson"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type DashboardResult struct {
|
||||
Accounts []mobilityaccountsstorage.Account
|
||||
Members []mobilityaccountsstorage.Account
|
||||
Events []agendastorage.Event
|
||||
Bookings []fleetstorage.Booking
|
||||
SolidarityDrivers []mobilityaccountsstorage.Account
|
||||
OrganizedCarpoolDrivers []mobilityaccountsstorage.Account
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetDashboardData(ctx context.Context, driverAddressGeoLayer, driverAddressGeoCode string) (*DashboardResult, error) {
|
||||
g := ctx.Value(identification.GroupKey)
|
||||
if g == nil {
|
||||
return nil, fmt.Errorf("no group found in context")
|
||||
}
|
||||
group := g.(storage.Group)
|
||||
|
||||
// Load geography polygons for driver address filtering
|
||||
var driverAddressPolygons []orb.Polygon
|
||||
if driverAddressGeoLayer != "" && driverAddressGeoCode != "" {
|
||||
polygons, err := h.loadGeographyPolygon(driverAddressGeoLayer, driverAddressGeoCode)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to load driver address geography filter")
|
||||
} else {
|
||||
driverAddressPolygons = polygons
|
||||
}
|
||||
}
|
||||
|
||||
// Get accounts (recent beneficiaries)
|
||||
request := &mobilityaccounts.GetAccountsBatchRequest{
|
||||
Accountids: group.Members,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accounts := []mobilityaccountsstorage.Account{}
|
||||
|
||||
// We only display the 5 most recent here
|
||||
count := len(resp.Accounts)
|
||||
min := count - 5
|
||||
if min < 0 {
|
||||
min = 0
|
||||
}
|
||||
|
||||
for _, account := range resp.Accounts[min:] {
|
||||
// Check if not archived
|
||||
if archived, ok := account.Data.AsMap()["archived"].(bool); !ok || !archived {
|
||||
a := account.ToStorageType()
|
||||
accounts = append([]mobilityaccountsstorage.Account{a}, accounts...)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch remaining data in parallel using goroutines
|
||||
var wg sync.WaitGroup
|
||||
var mu sync.Mutex
|
||||
|
||||
var members []mobilityaccountsstorage.Account
|
||||
var events []agendastorage.Event
|
||||
var bookings []fleetstorage.Booking
|
||||
var solidarityDrivers []mobilityaccountsstorage.Account
|
||||
var organizedCarpoolDrivers []mobilityaccountsstorage.Account
|
||||
|
||||
// Get members
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
m, _, err := h.groupmembers(group.ID)
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
members = m
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Get events
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
eventsresp, err := h.services.GRPC.Agenda.GetEvents(ctx, &agenda.GetEventsRequest{
|
||||
Namespaces: []string{"parcoursmob_dispositifs"},
|
||||
Mindate: timestamppb.Now(),
|
||||
})
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
for _, e := range eventsresp.Events {
|
||||
events = append(events, e.ToStorageType())
|
||||
}
|
||||
sort.Sort(sorting.EventsByStartdate(events))
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Get bookings
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
bookingsresp, err := h.services.GRPC.Fleets.GetBookings(ctx, &fleets.GetBookingsRequest{
|
||||
Namespaces: []string{"parcoursmob_dispositifs"},
|
||||
})
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
for _, b := range bookingsresp.Bookings {
|
||||
if b.Enddate.AsTime().After(time.Now()) {
|
||||
bookings = append(bookings, b.ToStorageType())
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Get solidarity transport drivers
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
solidarityRequest := &mobilityaccounts.GetAccountsRequest{
|
||||
Namespaces: []string{"solidarity_drivers"},
|
||||
}
|
||||
solidarityResp, err := h.services.GRPC.MobilityAccounts.GetAccounts(ctx, solidarityRequest)
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
for _, account := range solidarityResp.Accounts {
|
||||
// Only include non-archived drivers with addresses
|
||||
if archived, ok := account.Data.AsMap()["archived"].(bool); !ok || !archived {
|
||||
if address, ok := account.Data.AsMap()["address"]; ok && address != nil {
|
||||
// Apply geography filter if specified
|
||||
if len(driverAddressPolygons) > 0 {
|
||||
if addr, ok := account.Data.AsMap()["address"].(map[string]interface{}); ok {
|
||||
jsonAddr, err := json.Marshal(addr)
|
||||
if err == nil {
|
||||
addrGeojson, err := geojson.UnmarshalFeature(jsonAddr)
|
||||
if err == nil && addrGeojson.Geometry != nil {
|
||||
if point, ok := addrGeojson.Geometry.(orb.Point); ok {
|
||||
if isPointInGeographies(point, driverAddressPolygons) {
|
||||
solidarityDrivers = append(solidarityDrivers, account.ToStorageType())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
solidarityDrivers = append(solidarityDrivers, account.ToStorageType())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Get organized carpool drivers
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
carpoolRequest := &mobilityaccounts.GetAccountsRequest{
|
||||
Namespaces: []string{"organized_carpool_drivers"},
|
||||
}
|
||||
carpoolResp, err := h.services.GRPC.MobilityAccounts.GetAccounts(ctx, carpoolRequest)
|
||||
if err == nil {
|
||||
mu.Lock()
|
||||
for _, account := range carpoolResp.Accounts {
|
||||
// Only include non-archived drivers with addresses
|
||||
if archived, ok := account.Data.AsMap()["archived"].(bool); !ok || !archived {
|
||||
if address, ok := account.Data.AsMap()["address"]; ok && address != nil {
|
||||
// Apply geography filter if specified
|
||||
if len(driverAddressPolygons) > 0 {
|
||||
if addr, ok := account.Data.AsMap()["address"].(map[string]interface{}); ok {
|
||||
jsonAddr, err := json.Marshal(addr)
|
||||
if err == nil {
|
||||
addrGeojson, err := geojson.UnmarshalFeature(jsonAddr)
|
||||
if err == nil && addrGeojson.Geometry != nil {
|
||||
if point, ok := addrGeojson.Geometry.(orb.Point); ok {
|
||||
if isPointInGeographies(point, driverAddressPolygons) {
|
||||
organizedCarpoolDrivers = append(organizedCarpoolDrivers, account.ToStorageType())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
organizedCarpoolDrivers = append(organizedCarpoolDrivers, account.ToStorageType())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
mu.Unlock()
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for all goroutines to complete
|
||||
wg.Wait()
|
||||
|
||||
return &DashboardResult{
|
||||
Accounts: accounts,
|
||||
Members: members,
|
||||
Events: events,
|
||||
Bookings: bookings,
|
||||
SolidarityDrivers: solidarityDrivers,
|
||||
OrganizedCarpoolDrivers: organizedCarpoolDrivers,
|
||||
}, nil
|
||||
}
|
||||
|
||||
3
core/application/directory.go
Executable file
3
core/application/directory.go
Executable file
@@ -0,0 +1,3 @@
|
||||
package application
|
||||
|
||||
// Directory module - no business logic needed, all functionality moved to WebServer handlers
|
||||
171
core/application/documents.go
Normal file
171
core/application/documents.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
|
||||
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// DocumentConfig defines entity-specific document configuration
|
||||
type DocumentConfig struct {
|
||||
// Storage prefix for this entity type
|
||||
StoragePrefix string
|
||||
|
||||
// Namespace for account validation (empty if no validation needed)
|
||||
AccountNamespace string
|
||||
|
||||
// Whether to validate against MobilityAccounts service
|
||||
RequiresAccountValidation bool
|
||||
|
||||
// Custom validator function (optional)
|
||||
CustomValidator func(ctx context.Context, entityID string) error
|
||||
}
|
||||
|
||||
// Pre-configured document configs for each entity type
|
||||
var (
|
||||
BeneficiaryDocumentConfig = DocumentConfig{
|
||||
StoragePrefix: filestorage.PREFIX_BENEFICIARIES,
|
||||
AccountNamespace: "parcoursmob_beneficiaries",
|
||||
RequiresAccountValidation: true,
|
||||
}
|
||||
|
||||
SolidarityDriverDocumentConfig = DocumentConfig{
|
||||
StoragePrefix: filestorage.PREFIX_SOLIDARITY_TRANSPORT_DRIVERS,
|
||||
AccountNamespace: "solidarity_drivers",
|
||||
RequiresAccountValidation: true,
|
||||
}
|
||||
|
||||
OrganizedCarpoolDriverDocumentConfig = DocumentConfig{
|
||||
StoragePrefix: filestorage.PREFIX_ORGANIZED_CARPOOL_DRIVERS,
|
||||
AccountNamespace: "organized_carpool_drivers",
|
||||
RequiresAccountValidation: true,
|
||||
}
|
||||
|
||||
BookingDocumentConfig = DocumentConfig{
|
||||
StoragePrefix: filestorage.PREFIX_BOOKINGS,
|
||||
RequiresAccountValidation: false,
|
||||
}
|
||||
)
|
||||
|
||||
// AddDocument adds a document for any entity with validation
|
||||
func (h *ApplicationHandler) AddDocument(
|
||||
ctx context.Context,
|
||||
config DocumentConfig,
|
||||
entityID string,
|
||||
file io.Reader,
|
||||
filename string,
|
||||
fileSize int64,
|
||||
documentType string,
|
||||
documentName string,
|
||||
) error {
|
||||
// Perform validation if required
|
||||
if config.RequiresAccountValidation {
|
||||
if err := h.validateAccountForDocument(ctx, entityID, config.AccountNamespace); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validation if provided
|
||||
if config.CustomValidator != nil {
|
||||
if err := config.CustomValidator(ctx, entityID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Generate unique file ID
|
||||
fileid := uuid.NewString()
|
||||
|
||||
// Prepare metadata
|
||||
metadata := map[string]string{
|
||||
"type": documentType,
|
||||
"name": documentName,
|
||||
}
|
||||
|
||||
// Construct file path
|
||||
filepath := fmt.Sprintf("%s/%s_%s", entityID, fileid, filename)
|
||||
|
||||
// Store file
|
||||
return h.filestorage.Put(file, config.StoragePrefix, filepath, fileSize, metadata)
|
||||
}
|
||||
|
||||
// GetDocument retrieves a document for any entity with validation
|
||||
func (h *ApplicationHandler) GetDocument(
|
||||
ctx context.Context,
|
||||
config DocumentConfig,
|
||||
entityID string,
|
||||
document string,
|
||||
) (io.Reader, *filestorage.FileInfo, error) {
|
||||
// Perform validation if required
|
||||
if config.RequiresAccountValidation {
|
||||
if err := h.validateAccountForDocument(ctx, entityID, config.AccountNamespace); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validation if provided
|
||||
if config.CustomValidator != nil {
|
||||
if err := config.CustomValidator(ctx, entityID); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieve file
|
||||
filepath := fmt.Sprintf("%s/%s", entityID, document)
|
||||
return h.filestorage.Get(config.StoragePrefix, filepath)
|
||||
}
|
||||
|
||||
// ListDocuments retrieves all documents for an entity
|
||||
func (h *ApplicationHandler) ListDocuments(
|
||||
config DocumentConfig,
|
||||
entityID string,
|
||||
) []filestorage.FileInfo {
|
||||
prefix := fmt.Sprintf("%s/%s", config.StoragePrefix, entityID)
|
||||
return h.filestorage.List(prefix)
|
||||
}
|
||||
|
||||
// DeleteDocument deletes a document for any entity with validation
|
||||
func (h *ApplicationHandler) DeleteDocument(
|
||||
ctx context.Context,
|
||||
config DocumentConfig,
|
||||
entityID string,
|
||||
document string,
|
||||
) error {
|
||||
// Perform validation if required
|
||||
if config.RequiresAccountValidation {
|
||||
if err := h.validateAccountForDocument(ctx, entityID, config.AccountNamespace); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Custom validation if provided
|
||||
if config.CustomValidator != nil {
|
||||
if err := config.CustomValidator(ctx, entityID); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Delete file
|
||||
filepath := fmt.Sprintf("%s/%s", entityID, document)
|
||||
return h.filestorage.Delete(config.StoragePrefix, filepath)
|
||||
}
|
||||
|
||||
// validateAccountForDocument validates entity against MobilityAccounts service
|
||||
func (h *ApplicationHandler) validateAccountForDocument(ctx context.Context, accountID string, expectedNamespace string) error {
|
||||
resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, &mobilityaccounts.GetAccountRequest{
|
||||
Id: accountID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.Account.Namespace != expectedNamespace {
|
||||
return fmt.Errorf("account %s is not of type %s (namespace: %s)",
|
||||
accountID, expectedNamespace, resp.Account.Namespace)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
429
core/application/exports.go
Normal file
429
core/application/exports.go
Normal file
@@ -0,0 +1,429 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strconv"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
|
||||
agenda "git.coopgo.io/coopgo-platform/agenda/grpcapi"
|
||||
agendastorage "git.coopgo.io/coopgo-platform/agenda/storage"
|
||||
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"
|
||||
groupsstorage "git.coopgo.io/coopgo-platform/groups-management/storage"
|
||||
accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
|
||||
accountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
type FlatMaps []map[string]any
|
||||
|
||||
func (maps FlatMaps) GetHeaders() (res []string) {
|
||||
keys := map[string]bool{}
|
||||
for _, m := range maps {
|
||||
for k, _ := range m {
|
||||
if _, ok := keys[k]; !ok {
|
||||
keys[k] = true
|
||||
res = append(res, k)
|
||||
}
|
||||
}
|
||||
}
|
||||
sort.Strings(res)
|
||||
return
|
||||
}
|
||||
|
||||
func (maps FlatMaps) GetValues() (res [][]string) {
|
||||
headers := maps.GetHeaders()
|
||||
for _, m := range maps {
|
||||
line := []string{}
|
||||
for _, k := range headers {
|
||||
if v, ok := m[k]; ok && v != nil {
|
||||
line = append(line, fmt.Sprint(v))
|
||||
} else {
|
||||
line = append(line, "")
|
||||
}
|
||||
}
|
||||
res = append(res, line)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ExportCacheResult struct {
|
||||
Headers []string
|
||||
Values [][]string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) ExportCacheAsCSV(cacheID string) (*ExportCacheResult, error) {
|
||||
d, err := h.cache.Get(cacheID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var data []any
|
||||
if dataSlice, ok := d.([]any); ok {
|
||||
data = dataSlice
|
||||
} else {
|
||||
// Convert single item to slice
|
||||
jsonData, err := json.Marshal(d)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(jsonData, &data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
flatmaps := FlatMaps{}
|
||||
for _, v := range data {
|
||||
if vMap, ok := v.(map[string]any); ok {
|
||||
fm := map[string]any{}
|
||||
flatten("", vMap, fm)
|
||||
flatmaps = append(flatmaps, fm)
|
||||
}
|
||||
}
|
||||
|
||||
return &ExportCacheResult{
|
||||
Headers: flatmaps.GetHeaders(),
|
||||
Values: flatmaps.GetValues(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func flatten(prefix string, src map[string]any, dest map[string]any) {
|
||||
if len(prefix) > 0 {
|
||||
prefix += "."
|
||||
}
|
||||
for k, v := range src {
|
||||
switch child := v.(type) {
|
||||
case map[string]any:
|
||||
flatten(prefix+k, child, dest)
|
||||
case []any:
|
||||
for i := 0; i < len(child); i++ {
|
||||
dest[prefix+k+"."+strconv.Itoa(i)] = child[i]
|
||||
}
|
||||
default:
|
||||
dest[prefix+k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AgendaExportResult struct {
|
||||
ExcelFile *excelize.File
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) ExportAllAgendaEvents() (*AgendaExportResult, error) {
|
||||
resp, err := h.services.GRPC.Agenda.GetEvents(context.TODO(), &agenda.GetEventsRequest{
|
||||
Namespaces: []string{"parcoursmob_dispositifs"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := []agendastorage.Event{}
|
||||
groupids := []string{}
|
||||
beneficiaries_ids := []string{}
|
||||
|
||||
for _, e := range resp.Events {
|
||||
groupids = append(groupids, e.Owners...)
|
||||
events = append(events, e.ToStorageType())
|
||||
|
||||
for _, subscriptions := range e.Subscriptions {
|
||||
beneficiaries_ids = append(beneficiaries_ids, subscriptions.Subscriber)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sorting.EventsByStartdate(events))
|
||||
|
||||
groups, beneficiaries_map, err := h.getAgendaMetadata(groupids, beneficiaries_ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := h.generateAgendaExcel(events, groups, beneficiaries_map)
|
||||
return &AgendaExportResult{ExcelFile: file}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) ExportSingleAgendaEvent(eventID string) (*AgendaExportResult, error) {
|
||||
resp, err := h.services.GRPC.Agenda.GetEvent(context.TODO(), &agenda.GetEventRequest{
|
||||
Id: eventID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groupids := []string{}
|
||||
beneficiaries_ids := []string{}
|
||||
groupids = append(groupids, resp.Event.Owners...)
|
||||
|
||||
for _, subscriptions := range resp.Event.Subscriptions {
|
||||
beneficiaries_ids = append(beneficiaries_ids, subscriptions.Subscriber)
|
||||
}
|
||||
|
||||
groups, beneficiaries_map, err := h.getAgendaMetadata(groupids, beneficiaries_ids)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
events := []agendastorage.Event{resp.Event.ToStorageType()}
|
||||
file := h.generateAgendaExcel(events, groups, beneficiaries_map)
|
||||
return &AgendaExportResult{ExcelFile: file}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) getAgendaMetadata(groupids, beneficiaries_ids []string) (map[string]groupsstorage.Group, map[string]accountsstorage.Account, error) {
|
||||
groupsresp, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(context.TODO(), &groupsmanagement.GetGroupsBatchRequest{
|
||||
Groupids: groupids,
|
||||
})
|
||||
|
||||
groups := map[string]groupsstorage.Group{}
|
||||
if err == nil {
|
||||
for _, g := range groupsresp.Groups {
|
||||
groups[g.Id] = g.ToStorageType()
|
||||
}
|
||||
}
|
||||
|
||||
beneficiaries, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), &accounts.GetAccountsBatchRequest{
|
||||
Accountids: beneficiaries_ids,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
beneficiaries_map := map[string]accountsstorage.Account{}
|
||||
for _, ben := range beneficiaries.Accounts {
|
||||
beneficiaries_map[ben.Id] = ben.ToStorageType()
|
||||
}
|
||||
|
||||
return groups, beneficiaries_map, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) generateAgendaExcel(events []agendastorage.Event, groups map[string]groupsstorage.Group, beneficiaries_map map[string]accountsstorage.Account) *excelize.File {
|
||||
f := excelize.NewFile()
|
||||
|
||||
f.SetCellValue("Sheet1", "A1", "Evénement")
|
||||
f.SetCellValue("Sheet1", "B1", "Date de début")
|
||||
f.SetCellValue("Sheet1", "C1", "Date de fin")
|
||||
f.SetCellValue("Sheet1", "D1", "Nom bénéficiaire")
|
||||
f.SetCellValue("Sheet1", "E1", "Prenom bénéficiaire")
|
||||
f.SetCellValue("Sheet1", "F1", "Numéro allocataire / Pole emploi")
|
||||
f.SetCellValue("Sheet1", "G1", "Prescipteur")
|
||||
f.SetCellValue("Sheet1", "H1", "Prescipteur Nom")
|
||||
f.SetCellValue("Sheet1", "I1", "Prescipteur Email")
|
||||
f.SetCellValue("Sheet1", "J1", "Gestionnaire événement")
|
||||
|
||||
i := 2
|
||||
for _, e := range events {
|
||||
if len(e.Owners) == 0 {
|
||||
continue
|
||||
}
|
||||
admin := groups[e.Owners[0]]
|
||||
|
||||
for _, s := range e.Subscriptions {
|
||||
subscribedbygroup := ""
|
||||
subscribedbyuser := ""
|
||||
subscribedbyemail := ""
|
||||
if v, ok := s.Data["subscribed_by"].(map[string]any); ok {
|
||||
if v2, ok := v["group"].(map[string]any); ok {
|
||||
if v3, ok := v2["name"].(string); ok {
|
||||
subscribedbygroup = v3
|
||||
}
|
||||
}
|
||||
if v4, ok := v["user"].(map[string]any); ok {
|
||||
if v5, ok := v4["display_name"].(string); ok {
|
||||
subscribedbyuser = v5
|
||||
}
|
||||
if v6, ok := v4["email"].(string); ok {
|
||||
subscribedbyemail = v6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
beneficiary := beneficiaries_map[s.Subscriber]
|
||||
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i), e.Name)
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i), e.Startdate.Format("2006-01-02"))
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i), e.Enddate.Format("2006-01-02"))
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i), beneficiary.Data["last_name"])
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i), beneficiary.Data["first_name"])
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("F%d", i), beneficiary.Data["file_number"])
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("G%d", i), subscribedbygroup)
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("H%d", i), subscribedbyuser)
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("I%d", i), subscribedbyemail)
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("J%d", i), admin.Data["name"])
|
||||
i = i + 1
|
||||
}
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
type FleetBookingsExportResult struct {
|
||||
ExcelFile *excelize.File
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) ExportAllFleetBookings() (*FleetBookingsExportResult, error) {
|
||||
vehicles, bookings, err := h.getFleetData()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groups, beneficiaries_map, err := h.getFleetMetadata(bookings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := h.generateFleetExcel(bookings, vehicles, groups, beneficiaries_map, "")
|
||||
return &FleetBookingsExportResult{ExcelFile: file}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) ExportFleetBookingsByGroup(groupID string) (*FleetBookingsExportResult, error) {
|
||||
vehicles, bookings, err := h.getFleetData()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
groups, beneficiaries_map, err := h.getFleetMetadata(bookings)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
file := h.generateFleetExcel(bookings, vehicles, groups, beneficiaries_map, groupID)
|
||||
return &FleetBookingsExportResult{ExcelFile: file}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) getFleetData() (map[string]fleetsstorage.Vehicle, []fleetsstorage.Booking, error) {
|
||||
vehicles := map[string]fleetsstorage.Vehicle{}
|
||||
bookings := []fleetsstorage.Booking{}
|
||||
|
||||
request := &fleets.GetVehiclesRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
}
|
||||
resp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), request)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, vehicle := range resp.Vehicles {
|
||||
v := vehicle.ToStorageType()
|
||||
for _, b := range v.Bookings {
|
||||
bookings = append(bookings, b)
|
||||
}
|
||||
vehicles[vehicle.Id] = v
|
||||
}
|
||||
|
||||
return vehicles, bookings, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) getFleetMetadata(bookings []fleetsstorage.Booking) (map[string]groupsstorage.Group, map[string]accountsstorage.Account, error) {
|
||||
beneficiaries_ids := []string{}
|
||||
for _, b := range bookings {
|
||||
beneficiaries_ids = append(beneficiaries_ids, b.Driver)
|
||||
}
|
||||
|
||||
groups := map[string]groupsstorage.Group{}
|
||||
admingroups, err := h.services.GRPC.GroupsManagement.GetGroups(context.TODO(), &groupsmanagement.GetGroupsRequest{
|
||||
Namespaces: []string{"parcoursmob_organizations"},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, g := range admingroups.Groups {
|
||||
groups[g.Id] = g.ToStorageType()
|
||||
}
|
||||
|
||||
beneficiaries, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(context.TODO(), &accounts.GetAccountsBatchRequest{
|
||||
Accountids: beneficiaries_ids,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
beneficiaries_map := map[string]accountsstorage.Account{}
|
||||
for _, ben := range beneficiaries.Accounts {
|
||||
beneficiaries_map[ben.Id] = ben.ToStorageType()
|
||||
}
|
||||
|
||||
return groups, beneficiaries_map, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) generateFleetExcel(bookings []fleetsstorage.Booking, vehicles map[string]fleetsstorage.Vehicle, groups map[string]groupsstorage.Group, beneficiaries_map map[string]accountsstorage.Account, filterGroupID string) *excelize.File {
|
||||
f := excelize.NewFile()
|
||||
|
||||
f.SetCellValue("Sheet1", "A1", "Numéro")
|
||||
f.SetCellValue("Sheet1", "B1", "Type")
|
||||
f.SetCellValue("Sheet1", "C1", "Gestionnaire")
|
||||
f.SetCellValue("Sheet1", "D1", "Prescripteur")
|
||||
f.SetCellValue("Sheet1", "E1", "Bénéficiaire")
|
||||
f.SetCellValue("Sheet1", "F1", "Numéro allocataire / Pole emploi")
|
||||
f.SetCellValue("Sheet1", "G1", "Début de Mise à disposition")
|
||||
f.SetCellValue("Sheet1", "H1", "Fin de mise à disposition")
|
||||
f.SetCellValue("Sheet1", "I1", "Début indisponibilité")
|
||||
f.SetCellValue("Sheet1", "J1", "Fin indisponibilité")
|
||||
f.SetCellValue("Sheet1", "K1", "Véhicule retiré")
|
||||
f.SetCellValue("Sheet1", "L1", "Commentaire - Retrait véhicule")
|
||||
f.SetCellValue("Sheet1", "M1", "Réservation supprimée")
|
||||
f.SetCellValue("Sheet1", "N1", "Motif de la suppression")
|
||||
|
||||
i := 2
|
||||
for _, b := range bookings {
|
||||
vehicle := vehicles[b.Vehicleid]
|
||||
if len(vehicle.Administrators) == 0 {
|
||||
continue
|
||||
}
|
||||
admin := groups[vehicle.Administrators[0]]
|
||||
|
||||
bookedby := ""
|
||||
if v, ok := b.Data["booked_by"].(map[string]any); ok {
|
||||
if v2, ok := v["user"].(map[string]any); ok {
|
||||
if v3, ok := v2["display_name"].(string); ok {
|
||||
bookedby = v3
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bookedbygroup := ""
|
||||
if v4, ok := b.Data["booked_by"].(map[string]any); ok {
|
||||
if v5, ok := v4["group"].(map[string]any); ok {
|
||||
if v6, ok := v5["id"].(string); ok {
|
||||
bookedbygroup = v6
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by group if specified
|
||||
if filterGroupID != "" && bookedbygroup != filterGroupID {
|
||||
continue
|
||||
}
|
||||
|
||||
beneficiary := beneficiaries_map[b.Driver]
|
||||
adminunavailability := false
|
||||
|
||||
if av, ok := b.Data["administrator_unavailability"].(bool); ok && av {
|
||||
adminunavailability = true
|
||||
}
|
||||
|
||||
deleted := ""
|
||||
if b.Deleted {
|
||||
deleted = "DELETED"
|
||||
}
|
||||
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("A%d", i), vehicle.Data["licence_plate"])
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("B%d", i), vehicle.Type)
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("C%d", i), admin.Data["name"])
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("D%d", i), bookedby)
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("E%d", i), fmt.Sprintf("%v %v", beneficiary.Data["first_name"], beneficiary.Data["last_name"]))
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("F%d", i), beneficiary.Data["file_number"])
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("G%d", i), b.Startdate.Format("2006-01-02"))
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("H%d", i), b.Enddate.Format("2006-01-02"))
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("I%d", i), b.Unavailablefrom.Format("2006-01-02"))
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("J%d", i), b.Unavailableto.Format("2006-01-02"))
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("K%d", i), adminunavailability)
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("L%d", i), b.Data["comment"])
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("M%d", i), deleted)
|
||||
f.SetCellValue("Sheet1", fmt.Sprintf("N%d", i), b.Data["motif"])
|
||||
i = i + 1
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
133
core/application/group.go
Executable file
133
core/application/group.go
Executable file
@@ -0,0 +1,133 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
|
||||
"git.coopgo.io/coopgo-platform/groups-management/storage"
|
||||
accounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type GroupSettingsResult struct {
|
||||
Group storage.Group
|
||||
GroupMembers []any
|
||||
Admins []any
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetGroupSettings(ctx context.Context, groupID string) (*GroupSettingsResult, error) {
|
||||
// Get group info
|
||||
groupResp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, &groupsmanagement.GetGroupRequest{
|
||||
Id: groupID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get group: %w", err)
|
||||
}
|
||||
|
||||
group := groupResp.Group.ToStorageType()
|
||||
|
||||
members, err := h.members()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get members: %w", err)
|
||||
}
|
||||
|
||||
admins := []any{}
|
||||
groupMembers := []any{}
|
||||
|
||||
for _, m := range members {
|
||||
mm := m.ToStorageType()
|
||||
if groups, ok := mm.Data["groups"].([]any); ok {
|
||||
for _, g := range groups {
|
||||
if g.(string) == groupID {
|
||||
groupMembers = append(groupMembers, mm)
|
||||
}
|
||||
if g.(string) == groupID+":admin" {
|
||||
admins = append(admins, mm)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &GroupSettingsResult{
|
||||
Group: group,
|
||||
GroupMembers: groupMembers,
|
||||
Admins: admins,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) InviteMemberToGroup(ctx context.Context, groupID string, username string) error {
|
||||
// Get group info
|
||||
groupResp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, &groupsmanagement.GetGroupRequest{
|
||||
Id: groupID,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get group: %w", err)
|
||||
}
|
||||
|
||||
group := groupResp.Group.ToStorageType()
|
||||
|
||||
accountresp, err := h.services.GRPC.MobilityAccounts.GetAccountUsername(ctx, &accounts.GetAccountUsernameRequest{
|
||||
Username: username,
|
||||
Namespace: "parcoursmob",
|
||||
})
|
||||
|
||||
if err == nil {
|
||||
// Account already exists: adding the existing account to group
|
||||
account := accountresp.Account.ToStorageType()
|
||||
if account.Data["groups"] == nil {
|
||||
account.Data["groups"] = []any{}
|
||||
}
|
||||
account.Data["groups"] = append(account.Data["groups"].([]any), groupID)
|
||||
|
||||
as, err := accounts.AccountFromStorageType(&account)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert account: %w", err)
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, &accounts.UpdateDataRequest{
|
||||
Account: as,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update account: %w", err)
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"group": group.Data["name"],
|
||||
}
|
||||
|
||||
if err := h.emailing.Send("onboarding.existing_member", username, data); err != nil {
|
||||
log.Warn().Err(err).Msg("failed to send existing member email")
|
||||
}
|
||||
} else {
|
||||
// Create onboarding for new member
|
||||
onboarding := map[string]any{
|
||||
"username": username,
|
||||
"group": groupID,
|
||||
"admin": false,
|
||||
}
|
||||
|
||||
b := make([]byte, 16)
|
||||
if _, err := io.ReadFull(rand.Reader, b); err != nil {
|
||||
return fmt.Errorf("failed to generate random key: %w", err)
|
||||
}
|
||||
key := base64.RawURLEncoding.EncodeToString(b)
|
||||
|
||||
h.cache.PutWithTTL("onboarding/"+key, onboarding, 72*time.Hour)
|
||||
|
||||
data := map[string]any{
|
||||
"group": group.Data["name"],
|
||||
"key": key,
|
||||
}
|
||||
|
||||
if err := h.emailing.Send("onboarding.new_member", username, data); err != nil {
|
||||
return fmt.Errorf("failed to send new member email: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
219
core/application/group_module.go
Executable file
219
core/application/group_module.go
Executable file
@@ -0,0 +1,219 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
|
||||
groupstorage "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"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
type GroupsResult struct {
|
||||
Groups []groupstorage.Group
|
||||
}
|
||||
|
||||
type CreateGroupModuleResult struct {
|
||||
GroupID string
|
||||
}
|
||||
|
||||
type GroupModuleCreateDataResult struct {
|
||||
GroupTypes []string
|
||||
}
|
||||
|
||||
type DisplayGroupModuleResult struct {
|
||||
GroupID string
|
||||
Accounts []any
|
||||
CacheID string
|
||||
Searched bool
|
||||
Beneficiary any
|
||||
Group groupstorage.Group
|
||||
AccountsBeneficiaire []mobilityaccountsstorage.Account
|
||||
}
|
||||
|
||||
var Addres any
|
||||
|
||||
type BeneficiariesGroupForm 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"`
|
||||
PhoneNumber string `json:"phone_number" validate:"required,phoneNumber"`
|
||||
Address any `json:"address,omitempty"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
|
||||
type GroupsModuleByName []groupstorage.Group
|
||||
|
||||
func (a GroupsModuleByName) Len() int { return len(a) }
|
||||
func (a GroupsModuleByName) Less(i, j int) bool {
|
||||
return strings.Compare(a[i].Data["name"].(string), a[j].Data["name"].(string)) < 0
|
||||
}
|
||||
func (a GroupsModuleByName) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
func (h *ApplicationHandler) GetGroups(ctx context.Context) (*GroupsResult, error) {
|
||||
request := &groupsmanagement.GetGroupsRequest{
|
||||
Namespaces: []string{"parcoursmob_groups"},
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.GroupsManagement.GetGroups(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get groups: %w", err)
|
||||
}
|
||||
|
||||
var groups = []groupstorage.Group{}
|
||||
|
||||
for _, group := range resp.Groups {
|
||||
g := group.ToStorageType()
|
||||
groups = append(groups, g)
|
||||
}
|
||||
|
||||
sort.Sort(GroupsModuleByName(groups))
|
||||
|
||||
return &GroupsResult{
|
||||
Groups: groups,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetGroupModuleCreateData(ctx context.Context) (*GroupModuleCreateDataResult, error) {
|
||||
groupTypes := h.config.GetStringSlice("modules.groups.group_types")
|
||||
return &GroupModuleCreateDataResult{
|
||||
GroupTypes: groupTypes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) CreateGroupModule(ctx context.Context, name, groupType, description, address string) (*CreateGroupModuleResult, error) {
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("name is required")
|
||||
}
|
||||
if groupType == "" {
|
||||
return nil, fmt.Errorf("type is required")
|
||||
}
|
||||
|
||||
var addressData any
|
||||
if address != "" {
|
||||
if err := json.Unmarshal([]byte(address), &addressData); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse address: %w", err)
|
||||
}
|
||||
Addres = addressData
|
||||
}
|
||||
|
||||
groupID := uuid.NewString()
|
||||
|
||||
dataMap := map[string]any{
|
||||
"name": name,
|
||||
"type": groupType,
|
||||
"description": description,
|
||||
"address": Addres,
|
||||
}
|
||||
|
||||
data, err := structpb.NewValue(dataMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create data structure: %w", err)
|
||||
}
|
||||
|
||||
request := &groupsmanagement.AddGroupRequest{
|
||||
Group: &groupsmanagement.Group{
|
||||
Id: groupID,
|
||||
Namespace: "parcoursmob_groups",
|
||||
Data: data.GetStructValue(),
|
||||
},
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.GroupsManagement.AddGroup(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add group: %w", err)
|
||||
}
|
||||
|
||||
return &CreateGroupModuleResult{
|
||||
GroupID: groupID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func filterAccountBySearch(searchFilter string, a *mobilityaccounts.Account) bool {
|
||||
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
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
func (h *ApplicationHandler) DisplayGroupModule(ctx context.Context, groupID string, searchFilter string, currentUserGroup groupstorage.Group) (*DisplayGroupModuleResult, error) {
|
||||
request := &groupsmanagement.GetGroupRequest{
|
||||
Id: groupID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get group: %w", err)
|
||||
}
|
||||
|
||||
var accounts = []any{}
|
||||
|
||||
accountsRequest := &mobilityaccounts.GetAccountsBatchRequest{
|
||||
Accountids: resp.Group.Members,
|
||||
}
|
||||
|
||||
accountsResp, err := h.services.GRPC.MobilityAccounts.GetAccountsBatch(ctx, accountsRequest)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("failed to get accounts batch")
|
||||
} else {
|
||||
for _, account := range accountsResp.Accounts {
|
||||
if filterAccountBySearch(searchFilter, account) {
|
||||
a := account.ToStorageType()
|
||||
accounts = append(accounts, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cacheID := uuid.NewString()
|
||||
h.cache.PutWithTTL(cacheID, accounts, 1*time.Hour)
|
||||
|
||||
// Get beneficiaries in current user's group
|
||||
accountsBeneficiaire, err := h.services.GetBeneficiariesInGroup(currentUserGroup)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get beneficiaries in group: %w", err)
|
||||
}
|
||||
|
||||
return &DisplayGroupModuleResult{
|
||||
GroupID: resp.Group.ToStorageType().ID,
|
||||
Accounts: accounts,
|
||||
CacheID: cacheID,
|
||||
Searched: false,
|
||||
Beneficiary: nil,
|
||||
Group: resp.Group.ToStorageType(),
|
||||
AccountsBeneficiaire: accountsBeneficiaire,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) SubscribeBeneficiaryToGroup(ctx context.Context, groupID string, beneficiaryID string) error {
|
||||
beneficiaryRequest := &mobilityaccounts.GetAccountRequest{
|
||||
Id: beneficiaryID,
|
||||
}
|
||||
|
||||
beneficiaryResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, beneficiaryRequest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get beneficiary: %w", err)
|
||||
}
|
||||
|
||||
subscribe := &groupsmanagement.SubscribeRequest{
|
||||
Groupid: groupID,
|
||||
Memberid: beneficiaryResp.Account.Id,
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.GroupsManagement.Subscribe(ctx, subscribe)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to subscribe beneficiary to group: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
401
core/application/journeys.go
Executable file
401
core/application/journeys.go
Executable file
@@ -0,0 +1,401 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
|
||||
carpoolproto "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"
|
||||
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
"git.coopgo.io/coopgo-platform/multimodal-routing/libs/transit/transitous"
|
||||
savedsearchtypes "git.coopgo.io/coopgo-platform/saved-search/data/types"
|
||||
savedsearchproto "git.coopgo.io/coopgo-platform/saved-search/servers/grpc/proto/gen"
|
||||
savedsearchtransformers "git.coopgo.io/coopgo-platform/saved-search/servers/grpc/transformers"
|
||||
"git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
|
||||
"git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/transformers"
|
||||
"github.com/paulmach/orb/geojson"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type SearchJourneysResult struct {
|
||||
CarpoolResults []*geojson.FeatureCollection
|
||||
TransitResults []*transitous.Itinerary
|
||||
VehicleResults []fleetsstorage.Vehicle
|
||||
Searched bool
|
||||
DriverJourneys []*gen.SolidarityTransportDriverJourney
|
||||
Drivers map[string]mobilityaccountsstorage.Account
|
||||
OrganizedCarpools []*carpoolproto.CarpoolServiceDriverJourney
|
||||
KnowledgeBaseResults []any
|
||||
}
|
||||
|
||||
// SearchJourneys performs the business logic for journey search
|
||||
func (h *ApplicationHandler) SearchJourneys(
|
||||
ctx context.Context,
|
||||
departureDateTime time.Time,
|
||||
departureGeo *geojson.Feature,
|
||||
destinationGeo *geojson.Feature,
|
||||
passengerID string,
|
||||
solidarityTransportExcludeDriver string,
|
||||
) (*SearchJourneysResult, error) {
|
||||
var (
|
||||
// Results
|
||||
transitResults []*transitous.Itinerary
|
||||
carpoolResults []*geojson.FeatureCollection
|
||||
vehicleResults []fleetsstorage.Vehicle
|
||||
solidarityTransportResults []*gen.SolidarityTransportDriverJourney
|
||||
organizedCarpoolResults []*carpoolproto.CarpoolServiceDriverJourney
|
||||
knowledgeBaseResults []any
|
||||
|
||||
drivers = map[string]mobilityaccountsstorage.Account{}
|
||||
searched = false
|
||||
)
|
||||
|
||||
// Only search if we have complete departure and destination info
|
||||
if departureGeo != nil && destinationGeo != nil && !departureDateTime.IsZero() {
|
||||
searched = true
|
||||
|
||||
// SOLIDARITY TRANSPORT
|
||||
var err error
|
||||
drivers, err = h.services.GetAccountsInNamespacesMap([]string{"solidarity_drivers", "organized_carpool_drivers"})
|
||||
if err != nil {
|
||||
drivers = map[string]mobilityaccountsstorage.Account{}
|
||||
}
|
||||
|
||||
protodep, _ := transformers.GeoJsonToProto(departureGeo)
|
||||
protodest, _ := transformers.GeoJsonToProto(destinationGeo)
|
||||
|
||||
log.Debug().Time("departure time", departureDateTime).Msg("calling driver journeys with ...")
|
||||
|
||||
res, err := h.services.GRPC.SolidarityTransport.GetDriverJourneys(ctx, &gen.GetDriverJourneysRequest{
|
||||
Departure: protodep,
|
||||
Arrival: protodest,
|
||||
DepartureDate: timestamppb.New(departureDateTime),
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error in grpc call to GetDriverJourneys")
|
||||
} else {
|
||||
solidarityTransportResults = slices.Collect(func(yield func(*gen.SolidarityTransportDriverJourney) bool) {
|
||||
for _, dj := range res.DriverJourneys {
|
||||
if a, ok := drivers[dj.DriverId].Data["archived"]; ok {
|
||||
if archived, ok := a.(bool); ok {
|
||||
if archived {
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
if dj.DriverId == solidarityTransportExcludeDriver {
|
||||
continue
|
||||
}
|
||||
if !yield(dj) {
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
sort.Slice(solidarityTransportResults, func(i, j int) bool {
|
||||
return solidarityTransportResults[i].DriverDistance < solidarityTransportResults[j].DriverDistance
|
||||
})
|
||||
}
|
||||
|
||||
// Get departure and destination addresses from properties
|
||||
var departureAddress, destinationAddress string
|
||||
if departureGeo.Properties != nil {
|
||||
if label, ok := departureGeo.Properties["label"].(string); ok {
|
||||
departureAddress = label
|
||||
}
|
||||
}
|
||||
if destinationGeo.Properties != nil {
|
||||
if label, ok := destinationGeo.Properties["label"].(string); ok {
|
||||
destinationAddress = label
|
||||
}
|
||||
}
|
||||
|
||||
radius := float64(5)
|
||||
// ORGANIZED CARPOOL
|
||||
organizedCarpoolResultsRes, err := h.services.GRPC.CarpoolService.DriverJourneys(ctx, &carpoolproto.DriverJourneysRequest{
|
||||
DepartureLat: departureGeo.Point().Lat(),
|
||||
DepartureLng: departureGeo.Point().Lon(),
|
||||
ArrivalLat: destinationGeo.Point().Lat(),
|
||||
ArrivalLng: destinationGeo.Point().Lon(),
|
||||
DepartureDate: timestamppb.New(departureDateTime),
|
||||
DepartureAddress: &departureAddress,
|
||||
ArrivalAddress: &destinationAddress,
|
||||
DepartureRadius: &radius,
|
||||
ArrivalRadius: &radius,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving organized carpools")
|
||||
} else {
|
||||
organizedCarpoolResults = organizedCarpoolResultsRes.DriverJourneys
|
||||
sort.Slice(organizedCarpoolResults, func(i, j int) bool {
|
||||
return *organizedCarpoolResults[i].Distance < *organizedCarpoolResults[j].Distance
|
||||
})
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
// CARPOOL OPERATORS
|
||||
carpools := make(chan *geojson.FeatureCollection)
|
||||
go h.services.InteropCarpool.Search(carpools, *departureGeo, *destinationGeo, departureDateTime)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for c := range carpools {
|
||||
carpoolResults = append(carpoolResults, c)
|
||||
}
|
||||
}()
|
||||
|
||||
// TRANSIT
|
||||
transitch := make(chan *transitous.Itinerary)
|
||||
go func(transitch chan *transitous.Itinerary, departure *geojson.Feature, destination *geojson.Feature, datetime *time.Time) {
|
||||
defer close(transitch)
|
||||
response, err := h.services.TransitRouting.PlanWithResponse(ctx, &transitous.PlanParams{
|
||||
FromPlace: fmt.Sprintf("%f,%f", departure.Point().Lat(), departure.Point().Lon()),
|
||||
ToPlace: fmt.Sprintf("%f,%f", destination.Point().Lat(), destination.Point().Lon()),
|
||||
Time: datetime,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("error retrieving transit data from Transitous server")
|
||||
return
|
||||
}
|
||||
for _, i := range response.Itineraries {
|
||||
transitch <- &i
|
||||
}
|
||||
}(transitch, departureGeo, destinationGeo, &departureDateTime)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
paris, _ := time.LoadLocation("Europe/Paris")
|
||||
requestedDay := departureDateTime.In(paris).Truncate(24 * time.Hour)
|
||||
|
||||
for itinerary := range transitch {
|
||||
// Only include journeys that start on the requested day (in Paris timezone)
|
||||
if !itinerary.StartTime.IsZero() && !itinerary.EndTime.IsZero() {
|
||||
log.Info().
|
||||
Time("startTime", itinerary.StartTime).
|
||||
Time("endTime", itinerary.EndTime).
|
||||
Str("startTimezone", itinerary.StartTime.Location().String()).
|
||||
Str("endTimezone", itinerary.EndTime.Location().String()).
|
||||
Str("startTimeRFC3339", itinerary.StartTime.Format(time.RFC3339)).
|
||||
Str("endTimeRFC3339", itinerary.EndTime.Format(time.RFC3339)).
|
||||
Msg("Journey search - received transit itinerary from Transitous")
|
||||
|
||||
startInParis := itinerary.StartTime.In(paris)
|
||||
startDay := startInParis.Truncate(24 * time.Hour)
|
||||
|
||||
// Check if journey starts on the requested day
|
||||
if startDay.Equal(requestedDay) {
|
||||
transitResults = append(transitResults, itinerary)
|
||||
} else {
|
||||
log.Info().
|
||||
Str("requestedDay", requestedDay.Format("2006-01-02")).
|
||||
Str("startDay", startDay.Format("2006-01-02")).
|
||||
Msg("Journey search - filtered out transit journey (not on requested day)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// VEHICLES
|
||||
vehiclech := make(chan fleetsstorage.Vehicle)
|
||||
go h.vehicleRequest(vehiclech, departureDateTime.Add(-24*time.Hour), departureDateTime.Add(168*time.Hour))
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for vehicle := range vehiclech {
|
||||
vehicleResults = append(vehicleResults, vehicle)
|
||||
}
|
||||
slices.SortFunc(vehicleResults, sorting.VehiclesByDistanceFrom(*departureGeo))
|
||||
}()
|
||||
wg.Wait()
|
||||
|
||||
// KNOWLEDGE BASE
|
||||
departureGeoSearch, _ := h.services.Geography.GeoSearch(departureGeo)
|
||||
kbData := h.config.Get("knowledge_base")
|
||||
if kb, ok := kbData.([]any); ok {
|
||||
for _, sol := range kb {
|
||||
if solution, ok := sol.(map[string]any); ok {
|
||||
if g, ok := solution["geography"]; ok {
|
||||
if geography, ok := g.([]any); ok {
|
||||
for _, gg := range geography {
|
||||
if geog, ok := gg.(map[string]any); ok {
|
||||
if layer, ok := geog["layer"].(string); ok {
|
||||
code := geog["code"]
|
||||
geo, err := h.services.Geography.Find(layer, fmt.Sprintf("%v", code))
|
||||
if err == nil {
|
||||
geog["geography"] = geo
|
||||
geog["name"] = geo.Properties.MustString("nom")
|
||||
}
|
||||
if strings.Compare(fmt.Sprintf("%v", code), departureGeoSearch[layer].Properties.MustString("code")) == 0 {
|
||||
knowledgeBaseResults = append(knowledgeBaseResults, solution)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &SearchJourneysResult{
|
||||
CarpoolResults: carpoolResults,
|
||||
TransitResults: transitResults,
|
||||
VehicleResults: vehicleResults,
|
||||
Searched: searched,
|
||||
DriverJourneys: solidarityTransportResults,
|
||||
Drivers: drivers,
|
||||
OrganizedCarpools: organizedCarpoolResults,
|
||||
KnowledgeBaseResults: knowledgeBaseResults,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) vehicleRequest(vehiclech chan fleetsstorage.Vehicle, start time.Time, end time.Time) {
|
||||
defer close(vehiclech)
|
||||
vehiclerequest := &fleets.GetVehiclesRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
}
|
||||
vehicleresp, err := h.services.GRPC.Fleets.GetVehicles(context.TODO(), vehiclerequest)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("")
|
||||
return
|
||||
}
|
||||
for _, vehicle := range vehicleresp.Vehicles {
|
||||
v := vehicle.ToStorageType()
|
||||
if v.Free(start, end) {
|
||||
vehiclech <- v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SaveSearch saves a group's search to the saved-search microservice
|
||||
func (h *ApplicationHandler) SaveSearch(
|
||||
ctx context.Context,
|
||||
groupID string,
|
||||
departureDateTime time.Time,
|
||||
departureGeo *geojson.Feature,
|
||||
destinationGeo *geojson.Feature,
|
||||
additionalData map[string]interface{},
|
||||
) error {
|
||||
// Convert geojson.Feature to proto format
|
||||
var protoDepart, protoDest *savedsearchproto.SavedSearchGeoJsonFeature
|
||||
|
||||
log.Debug().
|
||||
Bool("departure_nil", departureGeo == nil).
|
||||
Bool("destination_nil", destinationGeo == nil).
|
||||
Msg("SaveSearch: checking geo features")
|
||||
|
||||
if departureGeo != nil {
|
||||
departureBytes, err := departureGeo.MarshalJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling departure: %w", err)
|
||||
}
|
||||
protoDepart = &savedsearchproto.SavedSearchGeoJsonFeature{
|
||||
Serialized: string(departureBytes),
|
||||
}
|
||||
log.Debug().Str("departure_json", string(departureBytes)).Msg("SaveSearch: departure converted")
|
||||
}
|
||||
|
||||
if destinationGeo != nil {
|
||||
destinationBytes, err := destinationGeo.MarshalJSON()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error marshaling destination: %w", err)
|
||||
}
|
||||
protoDest = &savedsearchproto.SavedSearchGeoJsonFeature{
|
||||
Serialized: string(destinationBytes),
|
||||
}
|
||||
log.Debug().Str("destination_json", string(destinationBytes)).Msg("SaveSearch: destination converted")
|
||||
}
|
||||
|
||||
// Convert additional data to protobuf Struct
|
||||
var protoData *structpb.Struct
|
||||
if additionalData != nil && len(additionalData) > 0 {
|
||||
var err error
|
||||
protoData, err = structpb.NewStruct(additionalData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error converting additional data: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle zero time value
|
||||
var protoDateTime *timestamppb.Timestamp
|
||||
if !departureDateTime.IsZero() {
|
||||
protoDateTime = timestamppb.New(departureDateTime)
|
||||
}
|
||||
|
||||
// Call the saved-search service
|
||||
_, err := h.services.GRPC.SavedSearch.CreateSavedSearch(ctx, &savedsearchproto.CreateSavedSearchRequest{
|
||||
OwnerId: groupID,
|
||||
Departure: protoDepart,
|
||||
Destination: protoDest,
|
||||
Datetime: protoDateTime,
|
||||
Data: protoData,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calling saved-search service: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Str("group_id", groupID).Msg("search saved successfully")
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSavedSearchesByOwner retrieves saved searches for a group
|
||||
func (h *ApplicationHandler) GetSavedSearchesByOwner(
|
||||
ctx context.Context,
|
||||
groupID string,
|
||||
) ([]*savedsearchtypes.SavedSearch, error) {
|
||||
// Call the saved-search service to get searches by owner
|
||||
response, err := h.services.GRPC.SavedSearch.GetSavedSearchesByOwner(ctx, &savedsearchproto.GetSavedSearchesByOwnerRequest{
|
||||
OwnerId: groupID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error calling saved-search service: %w", err)
|
||||
}
|
||||
|
||||
// Convert protobuf searches to domain types
|
||||
var searches []*savedsearchtypes.SavedSearch
|
||||
for _, protoSearch := range response.SavedSearches {
|
||||
search, err := savedsearchtransformers.SavedSearchProtoToType(protoSearch)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Str("search_id", protoSearch.Id).Msg("failed to convert saved search")
|
||||
continue
|
||||
}
|
||||
searches = append(searches, search)
|
||||
}
|
||||
|
||||
// Sort searches by datetime (earliest first)
|
||||
sort.Slice(searches, func(i, j int) bool {
|
||||
return searches[i].DateTime.Before(searches[j].DateTime)
|
||||
})
|
||||
|
||||
return searches, nil
|
||||
}
|
||||
|
||||
// DeleteSavedSearch deletes a saved search by ID for the specified owner
|
||||
func (h *ApplicationHandler) DeleteSavedSearch(
|
||||
ctx context.Context,
|
||||
searchID string,
|
||||
ownerID string,
|
||||
) error {
|
||||
// Call the saved-search service to delete the search
|
||||
_, err := h.services.GRPC.SavedSearch.DeleteSavedSearch(ctx, &savedsearchproto.DeleteSavedSearchRequest{
|
||||
Id: searchID,
|
||||
OwnerId: ownerID, // For authorization - ensure only the owner can delete
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("error calling saved-search service: %w", err)
|
||||
}
|
||||
|
||||
log.Info().Str("search_id", searchID).Str("owner_id", ownerID).Msg("saved search deleted successfully")
|
||||
return nil
|
||||
}
|
||||
247
core/application/members.go
Executable file
247
core/application/members.go
Executable file
@@ -0,0 +1,247 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
formvalidators "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/form-validators"
|
||||
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
|
||||
mobilityaccounts "git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
|
||||
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
)
|
||||
|
||||
type MembersResult struct {
|
||||
Accounts []mobilityaccountsstorage.Account
|
||||
CacheID string
|
||||
GroupsNames []string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetMembers(ctx context.Context) (*MembersResult, error) {
|
||||
accounts, err := h.services.GetAccounts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groupsNames []string
|
||||
|
||||
for _, v := range accounts {
|
||||
adminid := v.ID
|
||||
request := &mobilityaccounts.GetAccountRequest{
|
||||
Id: adminid,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var allIds []string
|
||||
for _, v := range resp.Account.ToStorageType().Data["groups"].([]any) {
|
||||
s := fmt.Sprintf("%v", v)
|
||||
if !(strings.Contains(s, "admin")) {
|
||||
allIds = append(allIds, s)
|
||||
}
|
||||
}
|
||||
|
||||
reques := &groupsmanagement.GetGroupsBatchRequest{
|
||||
Groupids: allIds,
|
||||
}
|
||||
|
||||
res, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(ctx, reques)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
g := ""
|
||||
for _, group := range res.Groups {
|
||||
g += fmt.Sprintf("%v", group.ToStorageType().Data["name"]) + " "
|
||||
}
|
||||
groupsNames = append(groupsNames, g)
|
||||
}
|
||||
|
||||
cacheID := uuid.NewString()
|
||||
h.cache.PutWithTTL(cacheID, accounts, 1*time.Hour)
|
||||
|
||||
return &MembersResult{
|
||||
Accounts: accounts,
|
||||
CacheID: cacheID,
|
||||
GroupsNames: groupsNames,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MemberDataResult struct {
|
||||
Account mobilityaccountsstorage.Account
|
||||
GroupsNames []string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetMemberData(ctx context.Context, memberID string) (*MemberDataResult, error) {
|
||||
request := &mobilityaccounts.GetAccountRequest{
|
||||
Id: memberID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Security check: ensure this is actually a member account
|
||||
if resp.Account.Namespace != "parcoursmob" {
|
||||
return nil, fmt.Errorf("account %s is not a member (namespace: %s)", memberID, resp.Account.Namespace)
|
||||
}
|
||||
|
||||
var allIds []string
|
||||
for _, v := range resp.Account.ToStorageType().Data["groups"].([]any) {
|
||||
s := fmt.Sprintf("%v", v)
|
||||
if !(strings.Contains(s, "admin")) {
|
||||
allIds = append(allIds, s)
|
||||
}
|
||||
}
|
||||
|
||||
reques := &groupsmanagement.GetGroupsBatchRequest{
|
||||
Groupids: allIds,
|
||||
}
|
||||
|
||||
res, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(ctx, reques)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var groupsNames []string
|
||||
for _, group := range res.Groups {
|
||||
g := fmt.Sprintf("%v", group.ToStorageType().Data["name"])
|
||||
groupsNames = append(groupsNames, g)
|
||||
}
|
||||
|
||||
return &MemberDataResult{
|
||||
Account: resp.Account.ToStorageType(),
|
||||
GroupsNames: groupsNames,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type MemberResult struct {
|
||||
Account mobilityaccountsstorage.Account
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetMember(ctx context.Context, memberID string) (*MemberResult, error) {
|
||||
request := &mobilityaccounts.GetAccountRequest{
|
||||
Id: memberID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Security check: ensure this is actually a member account
|
||||
if resp.Account.Namespace != "parcoursmob" {
|
||||
return nil, fmt.Errorf("account %s is not a member (namespace: %s)", memberID, resp.Account.Namespace)
|
||||
}
|
||||
|
||||
return &MemberResult{
|
||||
Account: resp.Account.ToStorageType(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) UpdateMember(ctx context.Context, memberID, firstName, lastName, email, phoneNumber, gender string) (string, error) {
|
||||
// Security check: verify the account exists and is a member
|
||||
getRequest := &mobilityaccounts.GetAccountRequest{
|
||||
Id: memberID,
|
||||
}
|
||||
getResp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, getRequest)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if getResp.Account.Namespace != "parcoursmob" {
|
||||
return "", fmt.Errorf("account %s is not a member (namespace: %s)", memberID, getResp.Account.Namespace)
|
||||
}
|
||||
|
||||
dataMap := map[string]any{
|
||||
"first_name": firstName,
|
||||
"last_name": lastName,
|
||||
"email": email,
|
||||
"phone_number": phoneNumber,
|
||||
"gender": gender,
|
||||
}
|
||||
|
||||
// Validate the data
|
||||
formData := UserForm{
|
||||
FirstName: firstName,
|
||||
LastName: lastName,
|
||||
Email: email,
|
||||
PhoneNumber: phoneNumber,
|
||||
Gender: gender,
|
||||
}
|
||||
|
||||
validate := formvalidators.New()
|
||||
if err := validate.Struct(formData); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
data, err := structpb.NewValue(dataMap)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
request := &mobilityaccounts.UpdateDataRequest{
|
||||
Account: &mobilityaccounts.Account{
|
||||
Id: memberID,
|
||||
Namespace: "parcoursmob",
|
||||
Data: data.GetStructValue(),
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.MobilityAccounts.UpdateData(ctx, request)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return resp.Account.Id, nil
|
||||
}
|
||||
|
||||
type UserForm struct {
|
||||
FirstName string `json:"first_name" validate:"required"`
|
||||
LastName string `json:"last_name" validate:"required"`
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
PhoneNumber string `json:"phone_number" `
|
||||
Address any `json:"address,omitempty"`
|
||||
Gender string `json:"gender"`
|
||||
}
|
||||
|
||||
type RegisterUserResult struct {
|
||||
UserID string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) RegisterUser(ctx context.Context, user mobilityaccountsstorage.Account) (*RegisterUserResult, error) {
|
||||
account, err := mobilityaccounts.AccountFromStorageType(&user)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.MobilityAccounts.Register(ctx, &mobilityaccounts.RegisterRequest{
|
||||
Account: account,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if g, ok := user.Metadata["import_in_group"]; ok {
|
||||
if group, ok := g.(string); ok {
|
||||
_, err = h.services.GRPC.GroupsManagement.Subscribe(ctx, &groupsmanagement.SubscribeRequest{
|
||||
Groupid: group,
|
||||
Memberid: resp.Account.Id,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &RegisterUserResult{
|
||||
UserID: resp.Account.Id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
1481
core/application/organized-carpool.go
Normal file
1481
core/application/organized-carpool.go
Normal file
File diff suppressed because it is too large
Load Diff
41
core/application/sms.go
Normal file
41
core/application/sms.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (h *ApplicationHandler) SendSMS(ctx context.Context, beneficiaryID, message string) error {
|
||||
return h.GenerateSMS(beneficiaryID, message)
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GenerateSMS(recipientid string, message string) error {
|
||||
recipient, err := h.services.GetAccount(recipientid)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("user not found")
|
||||
return err
|
||||
}
|
||||
|
||||
pn, ok := recipient.Data["phone_number"]
|
||||
if !ok {
|
||||
log.Error().Msg("Beneficiary doesn't have a phone number")
|
||||
return errors.New("missing phone number")
|
||||
}
|
||||
phoneNumber, ok := pn.(string)
|
||||
if !ok {
|
||||
log.Error().Msg("phone number type error")
|
||||
return errors.New("phone number type error")
|
||||
}
|
||||
|
||||
sender := h.config.GetString("service_name")
|
||||
|
||||
err = h.services.SMS.Send(phoneNumber, message, sender)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("cannot send SMS")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
1438
core/application/solidarity-transport.go
Normal file
1438
core/application/solidarity-transport.go
Normal file
File diff suppressed because it is too large
Load Diff
33
core/application/support.go
Executable file
33
core/application/support.go
Executable file
@@ -0,0 +1,33 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Content string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) SendSupportMessage(ctx context.Context, comment, userEmail string) error {
|
||||
data := map[string]any{
|
||||
"key": comment,
|
||||
"user": userEmail,
|
||||
}
|
||||
|
||||
supportEmail := h.config.GetString("modules.support.email")
|
||||
if supportEmail == "" {
|
||||
supportEmail = "support@mobicoop.fr"
|
||||
}
|
||||
|
||||
log.Debug().Str("user_email", userEmail).Str("support_email", supportEmail).Msg("Sending support message")
|
||||
|
||||
if err := h.emailing.Send("support.request", supportEmail, data); err != nil {
|
||||
return fmt.Errorf("failed to send support email: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
600
core/application/vehicles-management.go
Executable file
600
core/application/vehicles-management.go
Executable file
@@ -0,0 +1,600 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/identification"
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
|
||||
filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
|
||||
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"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type VehiclesManagementOverviewResult struct {
|
||||
Vehicles []fleetsstorage.Vehicle
|
||||
VehiclesMap map[string]fleetsstorage.Vehicle
|
||||
DriversMap map[string]mobilityaccountsstorage.Account
|
||||
Bookings []fleetsstorage.Booking
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetVehiclesManagementOverview(ctx context.Context, groupID string) (*VehiclesManagementOverviewResult, error) {
|
||||
request := &fleets.GetVehiclesRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
}
|
||||
resp, err := h.services.GRPC.Fleets.GetVehicles(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vehicles: %w", err)
|
||||
}
|
||||
|
||||
vehicles := []fleetsstorage.Vehicle{}
|
||||
bookings := []fleetsstorage.Booking{}
|
||||
vehiclesMap := map[string]fleetsstorage.Vehicle{}
|
||||
|
||||
for _, vehicle := range resp.Vehicles {
|
||||
if h.filterVehicleByGroup(vehicle, groupID) {
|
||||
v := vehicle.ToStorageType()
|
||||
vehicleBookings := []fleetsstorage.Booking{}
|
||||
for _, b := range v.Bookings {
|
||||
log.Debug().Any("booking", b).Msg("debug")
|
||||
if b.Status() != fleetsstorage.StatusOld {
|
||||
if !b.Deleted {
|
||||
bookings = append(bookings, b)
|
||||
}
|
||||
}
|
||||
if b.Unavailableto.After(time.Now()) {
|
||||
vehicleBookings = append(vehicleBookings, b)
|
||||
}
|
||||
}
|
||||
v.Bookings = vehicleBookings
|
||||
vehicles = append(vehicles, v)
|
||||
vehiclesMap[v.ID] = v
|
||||
}
|
||||
}
|
||||
|
||||
driversMap, _ := h.services.GetBeneficiariesMap()
|
||||
|
||||
sort.Sort(sorting.VehiclesByLicencePlate(vehicles))
|
||||
sort.Sort(sorting.BookingsByStartdate(bookings))
|
||||
|
||||
return &VehiclesManagementOverviewResult{
|
||||
Vehicles: vehicles,
|
||||
VehiclesMap: vehiclesMap,
|
||||
DriversMap: driversMap,
|
||||
Bookings: bookings,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) filterVehicleByGroup(v *fleets.Vehicle, groupID string) bool {
|
||||
if groupID == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, n := range v.Administrators {
|
||||
if n == groupID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type VehiclesManagementBookingsListResult struct {
|
||||
VehiclesMap map[string]fleetsstorage.Vehicle
|
||||
DriversMap map[string]mobilityaccountsstorage.Account
|
||||
Bookings []fleetsstorage.Booking
|
||||
CacheID string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetVehiclesManagementBookingsList(ctx context.Context, groupID, status, startDate, endDate string) (*VehiclesManagementBookingsListResult, error) {
|
||||
request := &fleets.GetVehiclesRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
IncludeDeleted: true,
|
||||
}
|
||||
resp, err := h.services.GRPC.Fleets.GetVehicles(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vehicles: %w", err)
|
||||
}
|
||||
|
||||
bookings := []fleetsstorage.Booking{}
|
||||
vehiclesMap := map[string]fleetsstorage.Vehicle{}
|
||||
|
||||
// Parse start date filter
|
||||
var startdate time.Time
|
||||
if startDate != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", startDate); err == nil {
|
||||
startdate = parsed
|
||||
}
|
||||
}
|
||||
|
||||
// Parse end date filter
|
||||
var enddate time.Time
|
||||
if endDate != "" {
|
||||
if parsed, err := time.Parse("2006-01-02", endDate); err == nil {
|
||||
enddate = parsed.Add(24 * time.Hour) // End of day
|
||||
}
|
||||
}
|
||||
|
||||
for _, vehicle := range resp.Vehicles {
|
||||
if h.filterVehicleByGroup(vehicle, groupID) {
|
||||
v := vehicle.ToStorageType()
|
||||
vehiclesMap[v.ID] = v
|
||||
for _, b := range v.Bookings {
|
||||
if v, ok := b.Data["administrator_unavailability"].(bool); !ok || !v {
|
||||
// Apply status filter
|
||||
if status != "" {
|
||||
bookingStatus := b.Status()
|
||||
statusInt := 0
|
||||
|
||||
if b.Deleted {
|
||||
statusInt = -2 // Use -2 for cancelled to distinguish from terminated
|
||||
} else {
|
||||
statusInt = bookingStatus
|
||||
}
|
||||
|
||||
// Map status string to int
|
||||
var filterStatusInt int
|
||||
switch status {
|
||||
case "FORTHCOMING":
|
||||
filterStatusInt = 1
|
||||
case "ONGOING":
|
||||
filterStatusInt = 0
|
||||
case "TERMINATED":
|
||||
filterStatusInt = -1
|
||||
case "CANCELLED":
|
||||
filterStatusInt = -2
|
||||
default:
|
||||
filterStatusInt = 999 // Invalid status, won't match anything
|
||||
}
|
||||
|
||||
if statusInt != filterStatusInt {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
// Apply date filter (on startdate)
|
||||
if !startdate.IsZero() && b.Startdate.Before(startdate) {
|
||||
continue
|
||||
}
|
||||
if !enddate.IsZero() && b.Startdate.After(enddate) {
|
||||
continue
|
||||
}
|
||||
|
||||
bookings = append(bookings, b)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sorting.BookingsByStartdate(bookings))
|
||||
|
||||
cacheID := uuid.NewString()
|
||||
h.cache.PutWithTTL(cacheID, bookings, 1*time.Hour)
|
||||
|
||||
driversMap, _ := h.services.GetBeneficiariesMap()
|
||||
|
||||
return &VehiclesManagementBookingsListResult{
|
||||
VehiclesMap: vehiclesMap,
|
||||
DriversMap: driversMap,
|
||||
Bookings: bookings,
|
||||
CacheID: cacheID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) CreateVehicle(ctx context.Context, name, vehicleType, informations, licencePlate, kilometers string, automatic bool, address map[string]any, otherProperties map[string]any) (string, error) {
|
||||
g := ctx.Value(identification.GroupKey)
|
||||
if g == nil {
|
||||
return "", fmt.Errorf("no group found in context")
|
||||
}
|
||||
group := g.(storage.Group)
|
||||
|
||||
dataMap := map[string]any{}
|
||||
if name != "" {
|
||||
dataMap["name"] = name
|
||||
}
|
||||
if address != nil {
|
||||
dataMap["address"] = address
|
||||
}
|
||||
if informations != "" {
|
||||
dataMap["informations"] = informations
|
||||
}
|
||||
if licencePlate != "" {
|
||||
dataMap["licence_plate"] = licencePlate
|
||||
}
|
||||
dataMap["automatic"] = automatic
|
||||
if kilometers != "" {
|
||||
dataMap["kilometers"] = kilometers
|
||||
}
|
||||
// Add other properties
|
||||
for key, value := range otherProperties {
|
||||
dataMap[key] = value
|
||||
}
|
||||
|
||||
data, err := structpb.NewValue(dataMap)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create data struct: %w", err)
|
||||
}
|
||||
|
||||
vehicle := &fleets.Vehicle{
|
||||
Id: uuid.NewString(),
|
||||
Namespace: "parcoursmob",
|
||||
Type: vehicleType,
|
||||
Administrators: []string{group.ID},
|
||||
Data: data.GetStructValue(),
|
||||
}
|
||||
|
||||
request := &fleets.AddVehicleRequest{
|
||||
Vehicle: vehicle,
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.Fleets.AddVehicle(ctx, request)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to add vehicle: %w", err)
|
||||
}
|
||||
|
||||
return vehicle.Id, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetVehicleTypes(ctx context.Context) ([]string, error) {
|
||||
return h.config.GetStringSlice("modules.fleets.vehicle_types"), nil
|
||||
}
|
||||
|
||||
type VehicleDisplayResult struct {
|
||||
Vehicle fleetsstorage.Vehicle
|
||||
Beneficiaries map[string]mobilityaccountsstorage.Account
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetVehicleDisplay(ctx context.Context, vehicleID string) (*VehicleDisplayResult, error) {
|
||||
request := &fleets.GetVehicleRequest{
|
||||
Vehicleid: vehicleID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Fleets.GetVehicle(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vehicle: %w", err)
|
||||
}
|
||||
|
||||
beneficiaries, err := h.services.GetBeneficiariesMap()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get beneficiaries: %w", err)
|
||||
}
|
||||
|
||||
vehicle := resp.Vehicle.ToStorageType()
|
||||
|
||||
// Sort bookings by start date (most recent first)
|
||||
sort.Slice(vehicle.Bookings, func(i, j int) bool {
|
||||
return vehicle.Bookings[i].Startdate.After(vehicle.Bookings[j].Startdate)
|
||||
})
|
||||
|
||||
return &VehicleDisplayResult{
|
||||
Vehicle: vehicle,
|
||||
Beneficiaries: beneficiaries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) UpdateBooking(ctx context.Context, bookingID, startdate, enddate, unavailablefrom, unavailableto string) error {
|
||||
booking, err := h.services.GetBooking(bookingID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get booking: %w", err)
|
||||
}
|
||||
|
||||
newbooking, _ := fleets.BookingFromStorageType(&booking)
|
||||
|
||||
if startdate != "" {
|
||||
newstartdate, _ := time.Parse("2006-01-02", startdate)
|
||||
newbooking.Startdate = timestamppb.New(newstartdate)
|
||||
|
||||
if newstartdate.Before(newbooking.Unavailablefrom.AsTime()) {
|
||||
newbooking.Unavailablefrom = timestamppb.New(newstartdate)
|
||||
}
|
||||
}
|
||||
|
||||
if enddate != "" {
|
||||
newenddate, _ := time.Parse("2006-01-02", enddate)
|
||||
newbooking.Enddate = timestamppb.New(newenddate)
|
||||
|
||||
if newenddate.After(newbooking.Unavailableto.AsTime()) || newenddate.Equal(newbooking.Unavailableto.AsTime()) {
|
||||
newbooking.Unavailableto = timestamppb.New(newenddate.Add(24 * time.Hour))
|
||||
}
|
||||
}
|
||||
|
||||
if unavailablefrom != "" {
|
||||
newunavailablefrom, _ := time.Parse("2006-01-02", unavailablefrom)
|
||||
newbooking.Unavailablefrom = timestamppb.New(newunavailablefrom)
|
||||
}
|
||||
|
||||
if unavailableto != "" {
|
||||
newunavailableto, _ := time.Parse("2006-01-02", unavailableto)
|
||||
newbooking.Unavailableto = timestamppb.New(newunavailableto)
|
||||
}
|
||||
|
||||
request := &fleets.UpdateBookingRequest{
|
||||
Booking: newbooking,
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.Fleets.UpdateBooking(ctx, request)
|
||||
return err
|
||||
}
|
||||
|
||||
type BookingDisplayResult struct {
|
||||
Booking fleetsstorage.Booking
|
||||
Vehicle fleetsstorage.Vehicle
|
||||
Beneficiary mobilityaccountsstorage.Account
|
||||
Group storage.Group
|
||||
Documents []filestorage.FileInfo
|
||||
FileTypesMap map[string]string
|
||||
Alternatives []any
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetBookingDisplay(ctx context.Context, bookingID string) (*BookingDisplayResult, error) {
|
||||
booking, err := h.services.GetBooking(bookingID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get booking: %w", err)
|
||||
}
|
||||
|
||||
beneficiary := mobilityaccountsstorage.Account{}
|
||||
if booking.Driver != "" {
|
||||
beneficiaryrequest := &mobilityaccounts.GetAccountRequest{
|
||||
Id: booking.Driver,
|
||||
}
|
||||
|
||||
beneficiaryresp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, beneficiaryrequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get beneficiary: %w", err)
|
||||
}
|
||||
beneficiary = beneficiaryresp.Account.ToStorageType()
|
||||
}
|
||||
|
||||
grouprequest := &groupsmanagement.GetGroupRequest{
|
||||
Id: booking.Vehicle.Administrators[0],
|
||||
}
|
||||
|
||||
groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, grouprequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get group: %w", err)
|
||||
}
|
||||
|
||||
alternativerequest := &fleets.GetVehiclesRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
Types: []string{booking.Vehicle.Type},
|
||||
Administrators: booking.Vehicle.Administrators,
|
||||
AvailabilityFrom: timestamppb.New(booking.Startdate),
|
||||
AvailabilityTo: timestamppb.New(booking.Enddate.Add(24 * time.Hour)),
|
||||
}
|
||||
|
||||
alternativeresp, err := h.services.GRPC.Fleets.GetVehicles(ctx, alternativerequest)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get alternative vehicles")
|
||||
}
|
||||
|
||||
alternatives := []any{}
|
||||
for _, a := range alternativeresp.Vehicles {
|
||||
alternatives = append(alternatives, a.ToStorageType())
|
||||
}
|
||||
|
||||
documents := h.filestorage.List(filestorage.PREFIX_BOOKINGS + "/" + bookingID)
|
||||
fileTypesMap := h.config.GetStringMapString("storage.files.file_types")
|
||||
|
||||
return &BookingDisplayResult{
|
||||
Booking: booking,
|
||||
Vehicle: booking.Vehicle,
|
||||
Beneficiary: beneficiary,
|
||||
Group: groupresp.Group.ToStorageType(),
|
||||
Documents: documents,
|
||||
FileTypesMap: fileTypesMap,
|
||||
Alternatives: alternatives,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) ChangeBookingVehicle(ctx context.Context, bookingID, newVehicleID string) error {
|
||||
booking, err := h.services.GetBooking(bookingID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get booking: %w", err)
|
||||
}
|
||||
|
||||
booking.Vehicleid = newVehicleID
|
||||
b, _ := fleets.BookingFromStorageType(&booking)
|
||||
|
||||
request := &fleets.UpdateBookingRequest{
|
||||
Booking: b,
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.Fleets.UpdateBooking(ctx, request)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) MakeVehicleUnavailable(ctx context.Context, vehicleID, unavailablefrom, unavailableto, comment, currentUserID string, currentUserClaims map[string]any) error {
|
||||
g := ctx.Value(identification.GroupKey)
|
||||
if g == nil {
|
||||
return fmt.Errorf("no group found in context")
|
||||
}
|
||||
currentGroup := g.(storage.Group)
|
||||
|
||||
unavailablefromTime, _ := time.Parse("2006-01-02", unavailablefrom)
|
||||
unavailabletoTime, _ := time.Parse("2006-01-02", unavailableto)
|
||||
|
||||
data := map[string]any{
|
||||
"comment": comment,
|
||||
"administrator_unavailability": true,
|
||||
"booked_by": map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": currentUserID,
|
||||
"display_name": currentUserClaims["display_name"],
|
||||
},
|
||||
"group": map[string]any{
|
||||
"id": currentGroup.ID,
|
||||
"name": currentGroup.Data["name"],
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
datapb, err := structpb.NewStruct(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create data struct: %w", err)
|
||||
}
|
||||
|
||||
booking := &fleets.Booking{
|
||||
Id: uuid.NewString(),
|
||||
Vehicleid: vehicleID,
|
||||
Unavailablefrom: timestamppb.New(unavailablefromTime),
|
||||
Unavailableto: timestamppb.New(unavailabletoTime),
|
||||
Data: datapb,
|
||||
}
|
||||
|
||||
request := &fleets.CreateBookingRequest{
|
||||
Booking: booking,
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.Fleets.CreateBooking(ctx, request)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) DeleteBooking(ctx context.Context, bookingID string) error {
|
||||
request := &fleets.DeleteBookingRequest{
|
||||
Id: bookingID,
|
||||
}
|
||||
|
||||
_, err := h.services.GRPC.Fleets.DeleteBooking(ctx, request)
|
||||
return err
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetBookingForUnbooking(ctx context.Context, bookingID string) (fleetsstorage.Booking, error) {
|
||||
request := &fleets.GetBookingRequest{
|
||||
Bookingid: bookingID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Fleets.GetBooking(ctx, request)
|
||||
if err != nil {
|
||||
return fleetsstorage.Booking{}, fmt.Errorf("failed to get booking: %w", err)
|
||||
}
|
||||
|
||||
return resp.Booking.ToStorageType(), nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) UnbookVehicle(ctx context.Context, bookingID, motif, currentUserID string, currentUserClaims map[string]any, currentGroup any) error {
|
||||
group := currentGroup.(storage.Group)
|
||||
|
||||
// Prepare deletion metadata (microservice will add deleted_at automatically)
|
||||
deletionMetadata := map[string]any{
|
||||
"deleted_by": map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": currentUserID,
|
||||
"display_name": currentUserClaims["first_name"].(string) + " " + currentUserClaims["last_name"].(string),
|
||||
"email": currentUserClaims["email"],
|
||||
},
|
||||
"group": map[string]any{
|
||||
"id": group.ID,
|
||||
"name": group.Data["name"],
|
||||
},
|
||||
},
|
||||
"reason": motif,
|
||||
}
|
||||
|
||||
deletionMetadataPb, err := structpb.NewStruct(deletionMetadata)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create deletion metadata: %w", err)
|
||||
}
|
||||
|
||||
// Use the microservice's delete endpoint with metadata
|
||||
deleteRequest := &fleets.DeleteBookingRequest{
|
||||
Id: bookingID,
|
||||
DeletionMetadata: deletionMetadataPb,
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.Fleets.DeleteBooking(ctx, deleteRequest)
|
||||
return err
|
||||
}
|
||||
|
||||
type VehicleForUpdateResult struct {
|
||||
Vehicle fleetsstorage.Vehicle
|
||||
VehicleTypes []string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetVehicleForUpdate(ctx context.Context, vehicleID string) (*VehicleForUpdateResult, error) {
|
||||
request := &fleets.GetVehicleRequest{
|
||||
Vehicleid: vehicleID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Fleets.GetVehicle(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vehicle: %w", err)
|
||||
}
|
||||
|
||||
vehicleTypes := h.config.GetStringSlice("modules.fleets.vehicle_types")
|
||||
|
||||
return &VehicleForUpdateResult{
|
||||
Vehicle: resp.Vehicle.ToStorageType(),
|
||||
VehicleTypes: vehicleTypes,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) UpdateVehicle(ctx context.Context, vehicleID, name, vehicleType, informations, licencePlate, kilometers string, automatic bool, address map[string]any, otherProperties map[string]any) (string, error) {
|
||||
getRequest := &fleets.GetVehicleRequest{
|
||||
Vehicleid: vehicleID,
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Fleets.GetVehicle(ctx, getRequest)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get vehicle: %w", err)
|
||||
}
|
||||
|
||||
// Start with existing data to preserve all fields
|
||||
dataMap := resp.Vehicle.Data.AsMap()
|
||||
if dataMap == nil {
|
||||
dataMap = map[string]any{}
|
||||
}
|
||||
|
||||
// Update with new values
|
||||
if name != "" {
|
||||
dataMap["name"] = name
|
||||
}
|
||||
if address != nil {
|
||||
dataMap["address"] = address
|
||||
}
|
||||
if informations != "" {
|
||||
dataMap["informations"] = informations
|
||||
}
|
||||
if licencePlate != "" {
|
||||
dataMap["licence_plate"] = licencePlate
|
||||
}
|
||||
if kilometers != "" {
|
||||
dataMap["kilometers"] = kilometers
|
||||
}
|
||||
dataMap["automatic"] = automatic
|
||||
// Add other properties
|
||||
for key, value := range otherProperties {
|
||||
dataMap[key] = value
|
||||
}
|
||||
|
||||
data, err := structpb.NewValue(dataMap)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create data struct: %w", err)
|
||||
}
|
||||
|
||||
updateRequest := &fleets.UpdateVehicleRequest{
|
||||
Vehicle: &fleets.Vehicle{
|
||||
Id: vehicleID,
|
||||
Namespace: resp.Vehicle.Namespace,
|
||||
Type: vehicleType,
|
||||
Administrators: resp.Vehicle.Administrators,
|
||||
Data: data.GetStructValue(),
|
||||
},
|
||||
}
|
||||
|
||||
updateResp, err := h.services.GRPC.Fleets.UpdateVehicle(ctx, updateRequest)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to update vehicle: %w", err)
|
||||
}
|
||||
|
||||
return updateResp.Vehicle.Id, nil
|
||||
}
|
||||
|
||||
393
core/application/vehicles.go
Executable file
393
core/application/vehicles.go
Executable file
@@ -0,0 +1,393 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-apps/parcoursmob/core/utils/sorting"
|
||||
filestorage "git.coopgo.io/coopgo-apps/parcoursmob/core/utils/storage"
|
||||
fleets "git.coopgo.io/coopgo-platform/fleets/grpcapi"
|
||||
"git.coopgo.io/coopgo-platform/fleets/storage"
|
||||
groupsmanagement "git.coopgo.io/coopgo-platform/groups-management/grpcapi"
|
||||
groupsmanagementstorage "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"
|
||||
"github.com/google/uuid"
|
||||
"github.com/paulmach/orb/geojson"
|
||||
"github.com/rs/zerolog/log"
|
||||
"google.golang.org/protobuf/types/known/structpb"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type VehiclesSearchResult struct {
|
||||
Vehicles []storage.Vehicle
|
||||
Beneficiary mobilityaccountsstorage.Account
|
||||
BeneficiaryDocuments []filestorage.FileInfo
|
||||
Groups map[string]any
|
||||
Searched bool
|
||||
StartDate string
|
||||
EndDate string
|
||||
VehicleType string
|
||||
Automatic bool
|
||||
MandatoryDocuments []string
|
||||
FileTypesMap map[string]string
|
||||
VehicleTypes []string
|
||||
Beneficiaries []mobilityaccountsstorage.Account
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) SearchVehicles(ctx context.Context, beneficiaryID, startDateStr, endDateStr, vehicleType string, automatic bool) (*VehiclesSearchResult, error) {
|
||||
var beneficiary mobilityaccountsstorage.Account
|
||||
beneficiarydocuments := []filestorage.FileInfo{}
|
||||
vehicles := []storage.Vehicle{}
|
||||
searched := false
|
||||
administrators := []string{}
|
||||
|
||||
startdate, err := time.Parse("2006-01-02", startDateStr)
|
||||
if err != nil {
|
||||
startdate = time.Time{}
|
||||
}
|
||||
enddate, err := time.Parse("2006-01-02", endDateStr)
|
||||
if err != nil {
|
||||
enddate = time.Time{}
|
||||
}
|
||||
|
||||
if beneficiaryID != "" && startdate.After(time.Now().Add(-24*time.Hour)) && enddate.After(startdate) {
|
||||
// Handler form
|
||||
searched = true
|
||||
|
||||
requestbeneficiary := &mobilityaccounts.GetAccountRequest{
|
||||
Id: beneficiaryID,
|
||||
}
|
||||
|
||||
respbeneficiary, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, requestbeneficiary)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get beneficiary: %w", err)
|
||||
}
|
||||
|
||||
beneficiary = respbeneficiary.Account.ToStorageType()
|
||||
|
||||
request := &fleets.GetVehiclesRequest{
|
||||
Namespaces: []string{"parcoursmob"},
|
||||
AvailabilityFrom: timestamppb.New(startdate),
|
||||
AvailabilityTo: timestamppb.New(enddate),
|
||||
}
|
||||
|
||||
if vehicleType != "" {
|
||||
request.Types = []string{vehicleType}
|
||||
}
|
||||
|
||||
resp, err := h.services.GRPC.Fleets.GetVehicles(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vehicles: %w", err)
|
||||
}
|
||||
|
||||
for _, vehicle := range resp.Vehicles {
|
||||
v := vehicle.ToStorageType()
|
||||
|
||||
if vehicleType == "Voiture" && automatic {
|
||||
if auto, ok := v.Data["automatic"].(bool); !ok || !auto {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
adminfound := false
|
||||
for _, a := range administrators {
|
||||
if a == v.Administrators[0] {
|
||||
adminfound = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !adminfound {
|
||||
administrators = append(administrators, v.Administrators[0])
|
||||
}
|
||||
|
||||
vehicles = append(vehicles, v)
|
||||
}
|
||||
|
||||
// Sort vehicles if beneficiary address is set
|
||||
if beneficiaryAddress, ok := beneficiary.Data["address"]; ok {
|
||||
beneficiaryAddressJson, err := json.Marshal(beneficiaryAddress)
|
||||
if err == nil {
|
||||
beneficiaryAddressGeojson, err := geojson.UnmarshalFeature(beneficiaryAddressJson)
|
||||
if err == nil {
|
||||
slices.SortFunc(vehicles, sorting.VehiclesByDistanceFrom(*beneficiaryAddressGeojson))
|
||||
} else {
|
||||
log.Error().Err(err).Msg("error transforming beneficiary address to GeoJSON")
|
||||
}
|
||||
} else {
|
||||
log.Error().Err(err).Msg("error transforming beneficiary address to JSON")
|
||||
}
|
||||
}
|
||||
|
||||
beneficiarydocuments = h.filestorage.List(filestorage.PREFIX_BENEFICIARIES + "/" + beneficiary.ID)
|
||||
}
|
||||
|
||||
accounts, err := h.services.GetBeneficiariesMap()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get beneficiaries: %w", err)
|
||||
}
|
||||
|
||||
// Convert map to slice for compatibility
|
||||
beneficiaries := make([]mobilityaccountsstorage.Account, 0, len(accounts))
|
||||
for _, account := range accounts {
|
||||
beneficiaries = append(beneficiaries, account)
|
||||
}
|
||||
|
||||
groups := map[string]any{}
|
||||
if len(administrators) > 0 {
|
||||
admingroups, err := h.services.GRPC.GroupsManagement.GetGroupsBatch(ctx, &groupsmanagement.GetGroupsBatchRequest{
|
||||
Groupids: administrators,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get admin groups: %w", err)
|
||||
}
|
||||
|
||||
for _, g := range admingroups.Groups {
|
||||
groups[g.Id] = g.ToStorageType()
|
||||
}
|
||||
}
|
||||
|
||||
sort.Sort(sorting.BeneficiariesByName(beneficiaries))
|
||||
|
||||
mandatoryDocuments := h.config.GetStringSlice("modules.fleets.booking_documents.mandatory")
|
||||
fileTypesMap := h.config.GetStringMapString("storage.files.file_types")
|
||||
vehicleTypes := h.config.GetStringSlice("modules.fleets.vehicle_types")
|
||||
|
||||
return &VehiclesSearchResult{
|
||||
Vehicles: vehicles,
|
||||
Beneficiary: beneficiary,
|
||||
BeneficiaryDocuments: beneficiarydocuments,
|
||||
Groups: groups,
|
||||
Searched: searched,
|
||||
StartDate: startDateStr,
|
||||
EndDate: endDateStr,
|
||||
VehicleType: vehicleType,
|
||||
Automatic: automatic,
|
||||
MandatoryDocuments: mandatoryDocuments,
|
||||
FileTypesMap: fileTypesMap,
|
||||
VehicleTypes: vehicleTypes,
|
||||
Beneficiaries: beneficiaries,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type BookVehicleResult struct {
|
||||
BookingID string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) BookVehicle(ctx context.Context, vehicleID, beneficiaryID, startDateStr, endDateStr string, documents map[string]io.Reader, documentHeaders map[string]string, existingDocs map[string]string, currentUserID string, currentUserClaims map[string]any, currentGroup any) (*BookVehicleResult, error) {
|
||||
group := currentGroup.(groupsmanagementstorage.Group)
|
||||
|
||||
vehicle, err := h.services.GRPC.Fleets.GetVehicle(ctx, &fleets.GetVehicleRequest{
|
||||
Vehicleid: vehicleID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("vehicle not found: %w", err)
|
||||
}
|
||||
|
||||
startdate, err := time.Parse("2006-01-02", startDateStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid start date: %w", err)
|
||||
}
|
||||
enddate, err := time.Parse("2006-01-02", endDateStr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid end date: %w", err)
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"booked_by": map[string]any{
|
||||
"user": map[string]any{
|
||||
"id": currentUserID,
|
||||
"display_name": fmt.Sprintf("%s %s", currentUserClaims["first_name"], currentUserClaims["last_name"]),
|
||||
},
|
||||
"group": map[string]any{
|
||||
"id": group.ID,
|
||||
"name": group.Data["name"],
|
||||
},
|
||||
},
|
||||
}
|
||||
datapb, err := structpb.NewStruct(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create booking metadata: %w", err)
|
||||
}
|
||||
|
||||
bookingID := uuid.NewString()
|
||||
booking := &fleets.Booking{
|
||||
Id: bookingID,
|
||||
Vehicleid: vehicleID,
|
||||
Driver: beneficiaryID,
|
||||
Startdate: timestamppb.New(startdate),
|
||||
Enddate: timestamppb.New(enddate),
|
||||
Unavailablefrom: timestamppb.New(startdate),
|
||||
Unavailableto: timestamppb.New(enddate.Add(72 * time.Hour)),
|
||||
Data: datapb,
|
||||
}
|
||||
|
||||
request := &fleets.CreateBookingRequest{
|
||||
Booking: booking,
|
||||
}
|
||||
|
||||
// Handle document uploads
|
||||
for docType, file := range documents {
|
||||
fileid := uuid.NewString()
|
||||
filename := documentHeaders[docType]
|
||||
|
||||
metadata := map[string]string{
|
||||
"type": docType,
|
||||
"name": filename,
|
||||
}
|
||||
|
||||
if err := h.filestorage.Put(file, filestorage.PREFIX_BOOKINGS, fmt.Sprintf("%s/%s_%s", bookingID, fileid, filename), -1, metadata); err != nil {
|
||||
return nil, fmt.Errorf("failed to upload document %s: %w", docType, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Handle existing documents
|
||||
for docType, existingFile := range existingDocs {
|
||||
path := strings.Split(existingFile, "/")
|
||||
if err := h.filestorage.Copy(existingFile, fmt.Sprintf("%s/%s/%s", filestorage.PREFIX_BOOKINGS, bookingID, path[len(path)-1])); err != nil {
|
||||
return nil, fmt.Errorf("failed to copy existing document %s: %w", docType, err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.Fleets.CreateBooking(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create booking: %w", err)
|
||||
}
|
||||
|
||||
// NOTIFY GROUP MEMBERS
|
||||
members, _, err := h.groupmembers(vehicle.Vehicle.Administrators[0])
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("failed to get group members for notification")
|
||||
} else {
|
||||
for _, m := range members {
|
||||
if email, ok := m.Data["email"].(string); ok {
|
||||
h.emailing.Send("fleets.bookings.creation_admin_alert", email, map[string]string{
|
||||
"bookingid": bookingID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return &BookVehicleResult{
|
||||
BookingID: bookingID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
type VehicleBookingDetailsResult struct {
|
||||
Booking storage.Booking
|
||||
Vehicle storage.Vehicle
|
||||
Beneficiary mobilityaccountsstorage.Account
|
||||
Group groupsmanagementstorage.Group
|
||||
Documents []filestorage.FileInfo
|
||||
FileTypesMap map[string]string
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetVehicleBookingDetails(ctx context.Context, bookingID string) (*VehicleBookingDetailsResult, error) {
|
||||
request := &fleets.GetBookingRequest{
|
||||
Bookingid: bookingID,
|
||||
}
|
||||
resp, err := h.services.GRPC.Fleets.GetBooking(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get booking: %w", err)
|
||||
}
|
||||
|
||||
booking := resp.Booking.ToStorageType()
|
||||
|
||||
beneficiaryrequest := &mobilityaccounts.GetAccountRequest{
|
||||
Id: booking.Driver,
|
||||
}
|
||||
|
||||
beneficiaryresp, err := h.services.GRPC.MobilityAccounts.GetAccount(ctx, beneficiaryrequest)
|
||||
if err != nil {
|
||||
beneficiaryresp = &mobilityaccounts.GetAccountResponse{
|
||||
Account: &mobilityaccounts.Account{},
|
||||
}
|
||||
}
|
||||
|
||||
grouprequest := &groupsmanagement.GetGroupRequest{
|
||||
Id: booking.Vehicle.Administrators[0],
|
||||
}
|
||||
|
||||
groupresp, err := h.services.GRPC.GroupsManagement.GetGroup(ctx, grouprequest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get group: %w", err)
|
||||
}
|
||||
|
||||
documents := h.filestorage.List(filestorage.PREFIX_BOOKINGS + "/" + bookingID)
|
||||
fileTypesMap := h.config.GetStringMapString("storage.files.file_types")
|
||||
|
||||
return &VehicleBookingDetailsResult{
|
||||
Booking: booking,
|
||||
Vehicle: booking.Vehicle,
|
||||
Beneficiary: beneficiaryresp.Account.ToStorageType(),
|
||||
Group: groupresp.Group.ToStorageType(),
|
||||
Documents: documents,
|
||||
FileTypesMap: fileTypesMap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type VehicleBookingsListResult struct {
|
||||
Bookings []storage.Booking
|
||||
VehiclesMap map[string]storage.Vehicle
|
||||
GroupsMap map[string]groupsmanagementstorage.Group
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetVehicleBookingsList(ctx context.Context, groupID string) (*VehicleBookingsListResult, error) {
|
||||
request := &fleets.GetBookingsRequest{}
|
||||
resp, err := h.services.GRPC.Fleets.GetBookings(ctx, request)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get bookings: %w", err)
|
||||
}
|
||||
|
||||
bookings := []storage.Booking{}
|
||||
|
||||
for _, b := range resp.Bookings {
|
||||
booking := b.ToStorageType()
|
||||
if b1, ok := booking.Data["booked_by"].(map[string]any); ok {
|
||||
if b2, ok := b1["group"].(map[string]any); ok {
|
||||
if b2["id"] == groupID {
|
||||
bookings = append(bookings, booking)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
vehiclesMap, err := h.services.GetVehiclesMap()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get vehicles map: %w", err)
|
||||
}
|
||||
|
||||
groupsMap, err := h.services.GetGroupsMap()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get groups map: %w", err)
|
||||
}
|
||||
|
||||
return &VehicleBookingsListResult{
|
||||
Bookings: bookings,
|
||||
VehiclesMap: vehiclesMap,
|
||||
GroupsMap: groupsMap,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *ApplicationHandler) GetBookingDocument(ctx context.Context, bookingID, document string) (io.Reader, string, error) {
|
||||
file, info, err := h.filestorage.Get(filestorage.PREFIX_BOOKINGS, fmt.Sprintf("%s/%s", bookingID, document))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to get document: %w", err)
|
||||
}
|
||||
|
||||
return file, info.ContentType, nil
|
||||
}
|
||||
|
||||
// Helper method to expose config to web handlers
|
||||
func (h *ApplicationHandler) GetConfig() interface{} {
|
||||
return h.config
|
||||
}
|
||||
146
core/application/wallets.go
Normal file
146
core/application/wallets.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
|
||||
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func (h *ApplicationHandler) CreditWallet(ctx context.Context, userid string, amount float64, paymentMethod string, description string) error {
|
||||
account, err := h.services.GetAccount(userid)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("could not retrieve account")
|
||||
return err
|
||||
}
|
||||
|
||||
// Initialize wallet if it doesn't exist
|
||||
if account.Data["wallet"] == nil {
|
||||
account.Data["wallet"] = float64(0)
|
||||
}
|
||||
|
||||
// Initialize wallet history if it doesn't exist
|
||||
if account.Data["wallet_history"] == nil {
|
||||
account.Data["wallet_history"] = []map[string]interface{}{}
|
||||
}
|
||||
|
||||
// Determine operation type based on amount sign
|
||||
operationType := "credit"
|
||||
if amount < 0 {
|
||||
operationType = "debit"
|
||||
}
|
||||
|
||||
// Create wallet operation record
|
||||
operation := map[string]interface{}{
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
"amount": amount,
|
||||
"payment_method": paymentMethod,
|
||||
"description": description,
|
||||
"operation_type": operationType,
|
||||
}
|
||||
|
||||
// Add operation to history
|
||||
var history []map[string]interface{}
|
||||
if existingHistory, ok := account.Data["wallet_history"].([]interface{}); ok {
|
||||
// Convert []interface{} to []map[string]interface{}
|
||||
for _, item := range existingHistory {
|
||||
if historyItem, ok := item.(map[string]interface{}); ok {
|
||||
history = append(history, historyItem)
|
||||
}
|
||||
}
|
||||
} else if existingHistory, ok := account.Data["wallet_history"].([]map[string]interface{}); ok {
|
||||
history = existingHistory
|
||||
}
|
||||
|
||||
history = append(history, operation)
|
||||
account.Data["wallet_history"] = history
|
||||
|
||||
log.Debug().
|
||||
Str("userid", userid).
|
||||
Float64("amount", amount).
|
||||
Str("paymentMethod", paymentMethod).
|
||||
Str("description", description).
|
||||
Int("historyCount", len(history)).
|
||||
Msg("Adding operation to wallet history")
|
||||
|
||||
// Note: wallet balance is NOT updated here - it remains as initial amount
|
||||
// Balance is calculated from initial amount + sum of all operations
|
||||
|
||||
accountproto, err := grpcapi.AccountFromStorageType(&account)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("account type transformation issue")
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = h.services.GRPC.MobilityAccounts.UpdateData(ctx, &grpcapi.UpdateDataRequest{
|
||||
Account: accountproto,
|
||||
})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("account update issue")
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info().
|
||||
Str("userid", userid).
|
||||
Float64("amount", amount).
|
||||
Str("payment_method", paymentMethod).
|
||||
Str("description", description).
|
||||
Msg("Wallet credited successfully")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// calculateWalletBalance calculates the current wallet balance from initial amount + all operations
|
||||
func (h *ApplicationHandler) calculateWalletBalance(account mobilityaccountsstorage.Account) float64 {
|
||||
// Return 0 if account data is nil
|
||||
if account.Data == nil {
|
||||
log.Debug().Msg("calculateWalletBalance: account.Data is nil, returning 0")
|
||||
return float64(0)
|
||||
}
|
||||
|
||||
// Get initial wallet amount (default to 0 if not set)
|
||||
initialAmount := float64(0)
|
||||
if walletValue, exists := account.Data["wallet"]; exists && walletValue != nil {
|
||||
if val, ok := walletValue.(float64); ok {
|
||||
initialAmount = val
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total from all operations
|
||||
operationsTotal := float64(0)
|
||||
operationCount := 0
|
||||
if historyValue, exists := account.Data["wallet_history"]; exists && historyValue != nil {
|
||||
var operations []map[string]interface{}
|
||||
|
||||
// Handle both []interface{} and []map[string]interface{} types
|
||||
if history, ok := historyValue.([]interface{}); ok {
|
||||
for _, item := range history {
|
||||
if operation, ok := item.(map[string]interface{}); ok {
|
||||
operations = append(operations, operation)
|
||||
}
|
||||
}
|
||||
} else if history, ok := historyValue.([]map[string]interface{}); ok {
|
||||
operations = history
|
||||
}
|
||||
|
||||
for _, operation := range operations {
|
||||
if amount, ok := operation["amount"].(float64); ok {
|
||||
operationsTotal += amount
|
||||
operationCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result := initialAmount + operationsTotal
|
||||
log.Debug().
|
||||
Str("accountId", account.ID).
|
||||
Float64("initialAmount", initialAmount).
|
||||
Float64("operationsTotal", operationsTotal).
|
||||
Int("operationCount", operationCount).
|
||||
Float64("result", result).
|
||||
Msg("calculateWalletBalance")
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user