Add history of operatons to wallets

This commit is contained in:
Arnaud Delcasse
2025-09-25 16:36:35 +02:00
parent 8c0b6f7d5c
commit 20a3b6ed3b
15 changed files with 612 additions and 163 deletions

View File

@@ -382,7 +382,11 @@ func (h *ApplicationHandler) BeneficiaryDisplay(w http.ResponseWriter, r *http.R
for i, d := range diag {
diagsAny[i] = d
}
h.Renderer.BeneficiaryDisplay(w, r, resp.Account.ToStorageType(), bookings, organizations, beneficiaries_file_types, file_types_map, documents, events_list, diagsAny, solidarityTransportStats)
account := resp.Account.ToStorageType()
walletBalance := h.calculateWalletBalance(account)
h.Renderer.BeneficiaryDisplay(w, r, account, bookings, organizations, beneficiaries_file_types, file_types_map, documents, events_list, diagsAny, solidarityTransportStats, walletBalance)
}
func (h *ApplicationHandler) BeneficiaryUpdate(w http.ResponseWriter, r *http.Request) {

View File

@@ -20,7 +20,7 @@ import (
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"
"git.coopgo.io/coopgo-platform/multimodal-routing/libs/transit/motis"
"git.coopgo.io/coopgo-platform/multimodal-routing/libs/transit/transitous"
"git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/proto/gen"
"git.coopgo.io/coopgo-platform/solidarity-transport/servers/grpc/transformers"
"github.com/google/uuid"
@@ -42,7 +42,7 @@ var (
func (h *ApplicationHandler) JourneysSearch(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
var (
transit_results []*motis.Itinerary
transit_results []*transitous.Itinerary
carpool_results []*geojson.FeatureCollection
vehicle_results []fleetsstorage.Vehicle
)
@@ -158,13 +158,28 @@ func (h *ApplicationHandler) JourneysSearch(w http.ResponseWriter, r *http.Reque
})
}
// 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
}
}
// ORGANIZED CARPOOL
organizedCarpoolsRes, err := h.services.GRPC.CarpoolService.DriverJourneys(context.Background(), &carpoolproto.DriverJourneysRequest{
DepartureLat: departuregeo.Point().Lat(),
DepartureLng: departuregeo.Point().Lon(),
ArrivalLat: destinationgeo.Point().Lat(),
ArrivalLng: destinationgeo.Point().Lon(),
DepartureDate: timestamppb.New(departuredatetime),
DepartureLat: departuregeo.Point().Lat(),
DepartureLng: departuregeo.Point().Lon(),
ArrivalLat: destinationgeo.Point().Lat(),
ArrivalLng: destinationgeo.Point().Lon(),
DepartureDate: timestamppb.New(departuredatetime),
DepartureAddress: &departureAddress,
ArrivalAddress: &destinationAddress,
})
if err != nil {
log.Error().Err(err).Msg("error retrieving organized carpools")
@@ -188,23 +203,19 @@ func (h *ApplicationHandler) JourneysSearch(w http.ResponseWriter, r *http.Reque
}()
// TRANSIT
transitch := make(chan *motis.Itinerary)
go func(transitch chan *motis.Itinerary, departure *geojson.Feature, destination *geojson.Feature, datetime *time.Time) {
transitch := make(chan *transitous.Itinerary)
go func(transitch chan *transitous.Itinerary, departure *geojson.Feature, destination *geojson.Feature, datetime *time.Time) {
defer close(transitch)
// timetableView := false
searchWindows := 1800
response, err := h.services.TransitRouting.PlanWithResponse(context.Background(), &motis.PlanParams{
response, err := h.services.TransitRouting.PlanWithResponse(context.Background(), &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,
// TimetableView: &timetableView,
SearchWindow: &searchWindows,
})
if err != nil {
log.Error().Err(err).Msg("error retrieving transit data from MOTIS server")
log.Error().Err(err).Msg("error retrieving transit data from Transitous server")
return
}
for _, i := range response.JSON200.Itineraries {
for _, i := range response.Itineraries {
transitch <- &i
}
}(transitch, departuregeo, destinationgeo, &departuredatetime)

View File

@@ -67,7 +67,6 @@ func (h *ApplicationHandler) OrganizedCarpoolOverview(w http.ResponseWriter, r *
if err == nil {
for _, b := range bookingsproto.Bookings {
// booking, _ := transformers.BookingProtoToType(b)
bookings = append(bookings, b)
}
}
@@ -85,6 +84,101 @@ func (h *ApplicationHandler) OrganizedCarpoolOverview(w http.ResponseWriter, r *
h.Renderer.OrganizedCarpoolOverview(w, r, accounts, accountsMap, beneficiariesMap, bookings)
}
func (h *ApplicationHandler) OrganizedCarpoolBookingDisplay(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bookingId := vars["bookingid"]
resp, err := h.services.GRPC.CarpoolService.GetBooking(context.Background(), &proto.GetCarpoolBookingRequest{
BookingId: bookingId,
})
if err != nil {
log.Error().Err(err).Msg("could not get carpool booking")
w.WriteHeader(http.StatusInternalServerError)
return
}
if resp.Booking == nil {
log.Error().Msg("carpool booking not found")
w.WriteHeader(http.StatusNotFound)
return
}
driver, err := h.services.GetAccount(resp.Booking.Driver.Id)
if err != nil {
log.Error().Err(err).Msg("driver retrieval issue")
w.WriteHeader(http.StatusInternalServerError)
return
}
passenger, err := h.services.GetAccount(resp.Booking.Passenger.Id)
if err != nil {
log.Error().Err(err).Msg("passenger retrieval issue")
w.WriteHeader(http.StatusInternalServerError)
return
}
// Extract driver departure and arrival addresses from DriverRoute GeoJSON
var driverDepartureAddress, driverArrivalAddress string
if resp.Booking.DriverRoute != nil && resp.Booking.DriverRoute.Serialized != "" {
fc, err := geojson.UnmarshalFeatureCollection([]byte(resp.Booking.DriverRoute.Serialized))
if err != nil {
log.Error().Err(err).Msg("could not unmarshal driver route geojson")
} else {
// Extract departure address (first feature)
if len(fc.Features) > 0 {
if addr, ok := fc.Features[0].Properties["label"]; ok {
if addrStr, ok := addr.(string); ok {
driverDepartureAddress = addrStr
}
}
}
// Extract arrival address (last feature)
if len(fc.Features) > 1 {
if addr, ok := fc.Features[1].Properties["label"]; ok {
if addrStr, ok := addr.(string); ok {
driverArrivalAddress = addrStr
}
}
}
}
}
h.Renderer.OrganizedCarpoolBookingDisplay(w, r, resp.Booking, driver, passenger, driverDepartureAddress, driverArrivalAddress)
}
func (h *ApplicationHandler) OrganizedCarpoolBookingStatus(action string) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
bookingId := vars["bookingid"]
var status proto.CarpoolServiceBookingStatus
if action == "confirm" {
status = proto.CarpoolServiceBookingStatus_CONFIRMED
} else if action == "cancel" {
status = proto.CarpoolServiceBookingStatus_CANCELLED
} else if action == "waitconfirmation" {
status = proto.CarpoolServiceBookingStatus_WAITING_DRIVER_CONFIRMATION
} else {
log.Error().Str("action", action).Msg("unknown booking action")
w.WriteHeader(http.StatusBadRequest)
return
}
_, err := h.services.GRPC.CarpoolService.UpdateBooking(context.Background(), &proto.UpdateCarpoolBookingRequest{
BookingId: bookingId,
Status: status,
})
if err != nil {
log.Error().Err(err).Msg("update carpool booking status issue")
w.WriteHeader(http.StatusInternalServerError)
return
}
// Redirect back to the booking display page
http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/bookings/%s", bookingId), http.StatusSeeOther)
}
}
func (h *ApplicationHandler) organizedCarpoolDrivers(r *http.Request) ([]mobilityaccountsstorage.Account, error) {
accounts := []mobilityaccountsstorage.Account{}
@@ -549,48 +643,200 @@ func (h *ApplicationHandler) OrganizedCarpoolJourney(w http.ResponseWriter, r *h
var passenger mobilityaccountsstorage.Account
passengerId := r.URL.Query().Get("passengerid")
log.Info().Str("journeyid", journeyId).Str("driverid", driverId).Str("passengerid", passengerId).Msg("driver journey")
log.Info().Str("journeyid", journeyId).Str("driverid", driverId).Str("passengerid", passengerId).Msg("organized carpool journey")
// Get the planned trip data
journeyResp, err := h.services.GRPC.CarpoolService.GetPlannedTrip(context.Background(), &proto.GetPlannedTripRequest{
Id: journeyId,
})
if err != nil {
log.Error().Err(err).Msg("could not get driver journey")
log.Error().Err(err).Msg("could not get carpool journey")
w.WriteHeader(http.StatusNotFound)
return
}
journey, err := geojson.UnmarshalFeatureCollection([]byte(journeyResp.PlannedTrip.Serialized))
if err != nil {
log.Error().Err(err).Msg("could not unmarshal driver journey")
log.Error().Err(err).Msg("could not unmarshal carpool journey")
w.WriteHeader(http.StatusNotFound)
return
}
departureDate := journey.ExtraMembers["departure_date"]
var departureTime *timestamppb.Timestamp
if departureDate != nil {
if dd, ok := departureDate.(string); ok {
dt, _ := time.Parse("2006-01-02 15:04", dd)
departureTime = timestamppb.New(dt)
}
}
// Extract operator from journey data
var operatorID string
if operator, ok := journey.ExtraMembers["operator"]; ok {
if op, ok := operator.(string); ok {
operatorID = op
}
}
if operatorID == "" {
operatorID = "example.coopgo.fr" // fallback to default
}
if departureTime == nil {
// Fallback to current time if we can't extract departure time
departureTime = timestamppb.New(time.Now())
}
if r.Method == "POST" {
if passengerId == "" {
log.Error().Err(err).Msg("could not get driver journey")
w.WriteHeader(http.StatusNotFound)
log.Error().Msg("missing passenger ID for carpool booking")
w.WriteHeader(http.StatusBadRequest)
return
}
/*if _, err := h.services.GRPC.CarpoolService.CreateBooking(context.Background(), &proto.CreateCarpoolBookingRequest{
Booking: &proto.CarpoolServiceBooking{
Passenger: &proto.CarpoolServiceUser{
Id: passengerId,
},
Driver: &proto.CarpoolServiceUser{
Id: driverId,
},
if err := r.ParseForm(); err != nil {
log.Error().Err(err).Msg("could not parse form input")
w.WriteHeader(http.StatusBadRequest)
return
}
// Extract journey properties from the geojson data
var journeyProps map[string]any
if props, ok := journey.ExtraMembers["properties"]; ok {
if propsMap, ok := props.(map[string]any); ok {
journeyProps = propsMap
}
PassengerId: passengerId,
DriverId: driverId,
DriverJourneyId: journeyId,
}); err != nil {
log.Error().Err(err).Msg("cannot create booking")
}
if journeyProps == nil {
log.Error().Msg("could not extract journey properties")
w.WriteHeader(http.StatusInternalServerError)
return
}*/
http.Redirect(w, r, fmt.Sprintf("/app/organized_carpool/"), http.StatusFound)
}
// Extract departure date from journey ExtraMembers
if depDate, ok := journey.ExtraMembers["departure_date"]; ok {
if depDateStr, ok := depDate.(string); ok {
if parsedTime, err := time.Parse("2006-01-02T15:04:05Z", depDateStr); err == nil {
departureTime = timestamppb.New(parsedTime)
} else if parsedTime, err := time.Parse("2006-01-02", depDateStr); err == nil {
departureTime = timestamppb.New(parsedTime)
} else {
log.Warn().Str("departure_date", depDateStr).Msg("could not parse departure date, using current time")
}
}
}
// Extract passenger pickup/drop coordinates and addresses from stored passenger data
var pickupLat, pickupLng, dropLat, dropLng float64
var pickupAddress, dropAddress string
// Check if we have passenger pickup and drop features in the journey's extra members
if pickupData, ok := journey.ExtraMembers["passenger_pickup"]; ok {
if pickupMap, ok := pickupData.(map[string]interface{}); ok {
if geometry, ok := pickupMap["geometry"].(map[string]interface{}); ok {
if coords, ok := geometry["coordinates"].([]interface{}); ok && len(coords) >= 2 {
if lng, ok := coords[0].(float64); ok {
pickupLng = lng
}
if lat, ok := coords[1].(float64); ok {
pickupLat = lat
}
}
}
if properties, ok := pickupMap["properties"].(map[string]interface{}); ok {
if label, ok := properties["label"].(string); ok {
pickupAddress = label
}
}
}
}
if dropData, ok := journey.ExtraMembers["passenger_drop"]; ok {
if dropMap, ok := dropData.(map[string]interface{}); ok {
if geometry, ok := dropMap["geometry"].(map[string]interface{}); ok {
if coords, ok := geometry["coordinates"].([]interface{}); ok && len(coords) >= 2 {
if lng, ok := coords[0].(float64); ok {
dropLng = lng
}
if lat, ok := coords[1].(float64); ok {
dropLat = lat
}
}
}
if properties, ok := dropMap["properties"].(map[string]interface{}); ok {
if label, ok := properties["label"].(string); ok {
dropAddress = label
}
}
}
}
log.Debug().Str("pickup_address", pickupAddress).Str("drop_address", dropAddress).Msg("Extracted addresses")
// Extract time from schedules if available and no specific departure_date was found
if departureTime.AsTime().Equal(time.Now().Truncate(time.Second)) {
if schedules, ok := journeyProps["schedules"]; ok {
if schedulesList, ok := schedules.([]any); ok && len(schedulesList) > 0 {
if schedule, ok := schedulesList[0].(map[string]any); ok {
if timeOfDay, ok := schedule["time_of_day"].(string); ok {
// Parse time and combine with current date
now := time.Now()
timeStr := fmt.Sprintf("%s %s", now.Format("2006-01-02"), timeOfDay)
if depTime, err := time.Parse("2006-01-02 15:04", timeStr); err == nil {
departureTime = timestamppb.New(depTime)
}
}
}
}
}
}
motivation := r.PostFormValue("motivation")
// Create carpool booking using extracted journey data
booking := &proto.CarpoolServiceBooking{
Id: uuid.NewString(),
Driver: &proto.CarpoolServiceUser{
Id: driverId,
Operator: operatorID,
},
Passenger: &proto.CarpoolServiceUser{
Id: passengerId,
Operator: operatorID,
},
PassengerPickupDate: departureTime,
PassengerPickupLat: pickupLat,
PassengerPickupLng: pickupLng,
PassengerDropLat: dropLat,
PassengerDropLng: dropLng,
PassengerPickupAddress: &pickupAddress,
PassengerDropAddress: &dropAddress,
Status: proto.CarpoolServiceBookingStatus_WAITING_DRIVER_CONFIRMATION,
DriverJourneyId: journeyId,
}
bookingRes, err := h.services.GRPC.CarpoolService.CreateBooking(context.Background(), &proto.CreateCarpoolBookingRequest{
Booking: booking,
})
if err != nil {
log.Error().Err(err).Msg("cannot create carpool booking")
w.WriteHeader(http.StatusInternalServerError)
return
}
log.Info().Str("booking_id", bookingRes.Booking.Id).Msg("Carpool booking created successfully")
// Send SMS notification if requested
message := r.PostFormValue("message")
doNotSend := r.PostFormValue("do_not_send")
if message != "" && doNotSend != "on" {
send_message := strings.ReplaceAll(message, "{booking_id}", bookingRes.Booking.Id)
send_message = strings.ReplaceAll(send_message, "{motivation}", motivation)
log.Debug().Str("message", send_message).Msg("Carpool booking created: sending message")
h.GenerateSMS(driverId, send_message)
}
http.Redirect(w, r, fmt.Sprintf("/app/organized-carpool/"), http.StatusFound)
return
}
@@ -616,13 +862,6 @@ func (h *ApplicationHandler) OrganizedCarpoolJourney(w http.ResponseWriter, r *h
return
}
/*driverjourney, err := transformers.DriverJourneyProtoToType(journey.DriverJourney)
if err != nil {
log.Error().Err(err).Msg("could not transform driver journey type")
w.WriteHeader(http.StatusBadRequest)
return
}*/
h.Renderer.OrganizedCarpoolJourney(w, r, journey, driver, passenger, beneficiaries)
}

View File

@@ -67,8 +67,13 @@ func (h *ApplicationHandler) SolidarityTransportExternalBookingProposal(w http.R
booking.Status = status
if status == "VALIDATED" {
h.GenerateSMS(passenger.ID, message)
if err := h.creditWallet(driver.ID, booking.Journey.Price.Amount, "Transport solidaire", "Crédit transport solidaire"); err != nil {
log.Error().Err(err).Msg("could not credit driver wallet")
w.WriteHeader(http.StatusInternalServerError)
return
}
} else if status == "CANCELLED" {
if err := h.creditWallet(passenger.ID, booking.Journey.Price.Amount); err != nil {
if err := h.creditWallet(passenger.ID, booking.Journey.Price.Amount, "Transport solidaire", "Remboursement annulation transport solidaire"); err != nil {
log.Error().Err(err).Msg("could not credit wallet")
w.WriteHeader(http.StatusInternalServerError)
return

View File

@@ -308,7 +308,9 @@ func (h *ApplicationHandler) SolidarityTransportDriverDisplay(w http.ResponseWri
},
}
h.Renderer.SolidarityTransportDriverDisplay(w, r, driver, availabilities, documents, bookings, stats)
walletBalance := h.calculateWalletBalance(driver)
h.Renderer.SolidarityTransportDriverDisplay(w, r, driver, availabilities, documents, bookings, stats, walletBalance)
}
func (h *ApplicationHandler) SolidarityTransportAddAvailability(w http.ResponseWriter, r *http.Request) {
@@ -724,8 +726,8 @@ func (h *ApplicationHandler) SolidarityTransportDriverJourney(w http.ResponseWri
if doNotSend != "on" {
h.GenerateSMS(driverId, send_message)
if err := h.creditWallet(passengerId, float64(-1)*journey.DriverJourney.Price.Amount); err != nil {
log.Error().Err(err).Msg("could not credit wallet")
if err := h.creditWallet(passengerId, float64(-1)*journey.DriverJourney.Price.Amount, "Transport solidaire", "Débit pour réservation transport solidaire"); err != nil {
log.Error().Err(err).Msg("could not credit passenger wallet")
w.WriteHeader(http.StatusInternalServerError)
return
}
@@ -756,7 +758,12 @@ func (h *ApplicationHandler) SolidarityTransportDriverJourney(w http.ResponseWri
return
}
h.Renderer.SolidarityTransportDriverJourney(w, r, driverjourney, driver, passenger, beneficiaries)
passengerWalletBalance := float64(0)
if passenger.Data != nil {
passengerWalletBalance = h.calculateWalletBalance(passenger)
}
h.Renderer.SolidarityTransportDriverJourney(w, r, driverjourney, driver, passenger, beneficiaries, passengerWalletBalance)
}
func (h *ApplicationHandler) SolidarityTransportDriverJourneyToggleNoreturn(w http.ResponseWriter, r *http.Request) {
@@ -830,7 +837,12 @@ func (h *ApplicationHandler) SolidarityTransportBookingDisplay(w http.ResponseWr
return
}
h.Renderer.SolidarityTransportBookingDisplay(w, r, booking, driver, passenger)
passengerWalletBalance := float64(0)
if passenger.Data != nil {
passengerWalletBalance = h.calculateWalletBalance(passenger)
}
h.Renderer.SolidarityTransportBookingDisplay(w, r, booking, driver, passenger, passengerWalletBalance)
}
func (h *ApplicationHandler) SolidarityTransportBookingStatus(action string) func(w http.ResponseWriter, r *http.Request) {
@@ -867,7 +879,7 @@ func (h *ApplicationHandler) SolidarityTransportBookingStatus(action string) fun
Id: bookingId,
})
if err == nil {
if err := h.creditWallet(booking.Booking.PassengerId, booking.Booking.Journey.Price.Amount); err != nil {
if err := h.creditWallet(booking.Booking.PassengerId, booking.Booking.Journey.Price.Amount, "Transport solidaire", "Remboursement annulation transport solidaire"); err != nil {
log.Error().Err(err).Msg("could not credit wallet")
w.WriteHeader(http.StatusInternalServerError)
return
@@ -879,13 +891,25 @@ func (h *ApplicationHandler) SolidarityTransportBookingStatus(action string) fun
Id: bookingId,
})
if err == nil {
if err := h.creditWallet(booking.Booking.PassengerId, float64(-1)*booking.Booking.Journey.Price.Amount); err != nil {
if err := h.creditWallet(booking.Booking.PassengerId, float64(-1)*booking.Booking.Journey.Price.Amount, "Transport solidaire", "Débit transport solidaire"); err != nil {
log.Error().Err(err).Msg("could not credit wallet")
w.WriteHeader(http.StatusInternalServerError)
return
}
}
}
if status == "VALIDATED" {
booking, err := h.services.GRPC.SolidarityTransport.GetSolidarityTransportBooking(context.Background(), &gen.GetSolidarityTransportBookingRequest{
Id: bookingId,
})
if err == nil {
if err := h.creditWallet(booking.Booking.DriverId, booking.Booking.Journey.Price.Amount, "Transport solidaire", "Crédit transport solidaire"); err != nil {
log.Error().Err(err).Msg("could not credit driver wallet")
w.WriteHeader(http.StatusInternalServerError)
return
}
}
}
}
http.Redirect(w, r, fmt.Sprintf("/app/solidarity-transport/bookings/%s", bookingId), http.StatusFound)
}

View File

@@ -4,8 +4,10 @@ import (
"context"
"net/http"
"strconv"
"time"
"git.coopgo.io/coopgo-platform/mobility-accounts/grpcapi"
mobilityaccountsstorage "git.coopgo.io/coopgo-platform/mobility-accounts/storage"
"github.com/gorilla/mux"
"github.com/rs/zerolog/log"
)
@@ -22,6 +24,8 @@ func (h ApplicationHandler) CreditWallet(w http.ResponseWriter, r *http.Request)
r.ParseForm()
amountStr := r.FormValue("amount")
paymentMethod := r.FormValue("payment_method")
description := r.FormValue("description")
amount, err := strconv.ParseFloat(amountStr, 64)
if err != nil {
@@ -30,7 +34,11 @@ func (h ApplicationHandler) CreditWallet(w http.ResponseWriter, r *http.Request)
return
}
if err := h.creditWallet(userid, amount); err != nil {
if paymentMethod == "" {
paymentMethod = "Paiement en espèce (MMS)"
}
if err := h.creditWallet(userid, amount, paymentMethod, description); err != nil {
log.Error().Err(err).Msg("could not credit wallet")
w.WriteHeader(http.StatusInternalServerError)
return
@@ -39,18 +47,65 @@ func (h ApplicationHandler) CreditWallet(w http.ResponseWriter, r *http.Request)
http.Redirect(w, r, r.Referer(), http.StatusFound)
}
func (h *ApplicationHandler) creditWallet(userid string, amount float64) error {
func (h *ApplicationHandler) creditWallet(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)
}
account.Data["wallet"] = account.Data["wallet"].(float64) + amount
// 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")
@@ -61,9 +116,69 @@ func (h *ApplicationHandler) creditWallet(userid string, amount float64) error {
Account: accountproto,
})
if err != nil {
log.Error().Err(err).Msg("account type transformation issue")
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
}