Логика из CreateOrder ручки была вынесена в OrderService.CreateOrder, так же Geocode вынесен в pkg, а ещё слегка переработана логика поиска координат складов
This commit is contained in:
164
internal/services/order.go
Normal file
164
internal/services/order.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/anxi0uz/logiflow/internal/api"
|
||||
"github.com/anxi0uz/logiflow/internal/config"
|
||||
"github.com/anxi0uz/logiflow/internal/models"
|
||||
storage "github.com/anxi0uz/logiflow/pkg"
|
||||
"github.com/anxi0uz/logiflow/pkg/geocode"
|
||||
"github.com/google/uuid"
|
||||
"github.com/huandu/go-sqlbuilder"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
type OrderService struct {
|
||||
db *pgxpool.Pool
|
||||
config config.Config
|
||||
}
|
||||
|
||||
type CreateOrderResult struct {
|
||||
Order models.Order
|
||||
Route models.Route
|
||||
}
|
||||
|
||||
func NewOrderService(db *pgxpool.Pool, cfg config.Config) *OrderService {
|
||||
return &OrderService{db: db, config: cfg}
|
||||
}
|
||||
|
||||
func (s *OrderService) CreateOrder(ctx context.Context, req api.OrderCreate, userID uuid.UUID) (*CreateOrderResult, error) {
|
||||
var (
|
||||
originLat, originLon float64
|
||||
destLat, destLon float64
|
||||
)
|
||||
g, gctx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
if req.OriginWarehouseId != nil {
|
||||
wh, err := storage.GetOne[models.Warehouse](gctx, s.db, "warehouses", func(sb *sqlbuilder.SelectBuilder) {
|
||||
sb.Where(sb.Equal("id", req.OriginWarehouseId))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
originLat = wh.Latitude
|
||||
originLon = wh.Longitude
|
||||
return nil
|
||||
}
|
||||
|
||||
var err error
|
||||
originLat, originLon, err = geocode.Geocode(gctx, *req.OriginAddress)
|
||||
return err
|
||||
})
|
||||
g.Go(func() error {
|
||||
if req.DestinationWarehouseId != nil {
|
||||
wh, err := storage.GetOne[models.Warehouse](gctx, s.db, "warehouses", func(sb *sqlbuilder.SelectBuilder) {
|
||||
sb.Where(sb.Equal("id", req.DestinationWarehouseId))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
destLat = wh.Latitude
|
||||
destLon = wh.Longitude
|
||||
return nil
|
||||
}
|
||||
var err error
|
||||
destLat, originLon, err = geocode.Geocode(ctx, req.DestinationAddress)
|
||||
return err
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
slog.ErrorContext(ctx, "error while getting coordinates", slog.String("error", err.Error()))
|
||||
return nil, fmt.Errorf("error while getting coordinates: %w", err)
|
||||
}
|
||||
osrmURL := fmt.Sprintf(
|
||||
"http://router.project-osrm.org/route/v1/driving/%f,%f;%f,%f?overview=full&geometries=geojson",
|
||||
originLon, originLat, destLon, destLat,
|
||||
)
|
||||
osrmReq, _ := http.NewRequestWithContext(ctx, http.MethodGet, osrmURL, nil)
|
||||
osrmReq.Header.Set("User-Agent", "logiflow/1.0")
|
||||
|
||||
osrmResp, err := http.DefaultClient.Do(osrmReq)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("osrm request: %w", err)
|
||||
}
|
||||
defer osrmResp.Body.Close()
|
||||
|
||||
var osrmResult geocode.OsrmResult
|
||||
if err := json.NewDecoder(osrmResp.Body).Decode(&osrmResult); err != nil || len(osrmResult.Routes) == 0 {
|
||||
return nil, fmt.Errorf("osrm: no route found")
|
||||
}
|
||||
route := osrmResult.Routes[0]
|
||||
distanceKm := route.Distance / 1000
|
||||
|
||||
var weightKg, volumeM3 float64
|
||||
if req.WeightKg != nil {
|
||||
weightKg = float64(*req.WeightKg)
|
||||
}
|
||||
if req.VolumeM3 != nil {
|
||||
volumeM3 = float64(*req.VolumeM3)
|
||||
}
|
||||
price := s.config.Pricing.BaseFee +
|
||||
distanceKm*s.config.Pricing.PerKm +
|
||||
weightKg*s.config.Pricing.PerKg +
|
||||
volumeM3*s.config.Pricing.PerM3
|
||||
|
||||
orderID := uuid.New()
|
||||
order := models.Order{
|
||||
ID: orderID,
|
||||
CreatedByID: &userID,
|
||||
DestinationAddress: req.DestinationAddress,
|
||||
Status: "pending",
|
||||
TotalPrice: price,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if req.OriginAddress != nil && *req.OriginAddress != "" {
|
||||
order.OriginAddress = *req.OriginAddress
|
||||
}
|
||||
if req.CargoDescription != nil {
|
||||
order.CargoDescription = *req.CargoDescription
|
||||
}
|
||||
if req.WeightKg != nil {
|
||||
order.WeightKg = weightKg
|
||||
}
|
||||
if req.VolumeM3 != nil {
|
||||
order.VolumeM3 = volumeM3
|
||||
}
|
||||
|
||||
coordsJSON, err := json.Marshal(route.Geometry.Coordinates)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("marshal coordinates: %w", err)
|
||||
}
|
||||
routeModel := models.Route{
|
||||
ID: uuid.New(),
|
||||
OrderID: orderID,
|
||||
Coordinates: coordsJSON,
|
||||
DurationSec: int(route.Duration),
|
||||
Status: "pending",
|
||||
}
|
||||
|
||||
tx, err := s.db.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("begin tx: %w", err)
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
if err := storage.Create(ctx, "orders", order, tx); err != nil {
|
||||
return nil, fmt.Errorf("create order: %w", err)
|
||||
}
|
||||
if err := storage.Create(ctx, "routes", routeModel, tx); err != nil {
|
||||
return nil, fmt.Errorf("create route: %w", err)
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, fmt.Errorf("commit: %w", err)
|
||||
}
|
||||
|
||||
return &CreateOrderResult{Order: order, Route: routeModel}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user