dictyBase/modware-order

View on GitHub
internal/app/service/service.go

Summary

Maintainability
B
4 hrs
Test Coverage
package service

import (
    "context"
    "fmt"
    "time"

    "github.com/dictyBase/apihelpers/aphgrpc"
    "github.com/dictyBase/arangomanager/query"
    "github.com/dictyBase/go-genproto/dictybaseapis/order"
    "github.com/dictyBase/modware-order/internal/message"
    "github.com/dictyBase/modware-order/internal/model"
    "github.com/dictyBase/modware-order/internal/repository"
    "github.com/dictyBase/modware-order/internal/repository/arangodb"
    "github.com/golang/protobuf/ptypes/empty"
)

const Divider = 1000000

// OrderService is the container for managing order service definition.
type OrderService struct {
    order.UnimplementedOrderServiceServer
    *aphgrpc.Service
    repo      repository.OrderRepository
    publisher message.Publisher
}

func defaultOptions() *aphgrpc.ServiceOptions {
    return &aphgrpc.ServiceOptions{Resource: "order"}
}

// NewOrderService is the constructor for creating a new instance of
// OrderService.
func NewOrderService(
    repo repository.OrderRepository,
    pub message.Publisher,
    opt ...aphgrpc.Option,
) *OrderService {
    so := defaultOptions()
    for _, optfn := range opt {
        optfn(so)
    }
    srv := &aphgrpc.Service{}
    aphgrpc.AssignFieldsToStructs(so, srv)

    return &OrderService{
        Service:   srv,
        repo:      repo,
        publisher: pub,
    }
}

// GetOrder handles getting an order by ID.
func (s *OrderService) GetOrder(
    ctx context.Context,
    rdr *order.OrderId,
) (*order.Order, error) {
    ord := &order.Order{}
    if err := rdr.Validate(); err != nil {
        return ord, aphgrpc.HandleInvalidParamError(ctx, err)
    }
    mord, err := s.repo.GetOrder(rdr.Id)
    if err != nil {
        return ord, aphgrpc.HandleGetError(ctx, err)
    }
    if mord.NotFound {
        return ord, aphgrpc.HandleNotFoundError(ctx, err)
    }
    ord.Data = s.orderData(mord)

    return ord, nil
}

// CreateOrder handles the creation of a new order.
func (s *OrderService) CreateOrder(
    ctx context.Context,
    rdr *order.NewOrder,
) (*order.Order, error) {
    ord := &order.Order{}
    if err := rdr.Validate(); err != nil {
        return ord, aphgrpc.HandleInvalidParamError(ctx, err)
    }
    adr, err := s.repo.AddOrder(rdr)
    if err != nil {
        return ord, aphgrpc.HandleInsertError(ctx, err)
    }
    if adr.NotFound {
        return ord, aphgrpc.HandleNotFoundError(ctx, err)
    }
    ord.Data = s.orderData(adr)
    s.publisher.Publish(s.Topics["orderCreate"], ord) //nolint:errcheck

    return ord, nil
}

// UpdateOrder handles updating an existing order.
func (s *OrderService) UpdateOrder(
    ctx context.Context,
    urd *order.OrderUpdate,
) (*order.Order, error) {
    ord := &order.Order{}
    if err := urd.Validate(); err != nil {
        return ord, aphgrpc.HandleInvalidParamError(ctx, err)
    }
    mord, err := s.repo.EditOrder(urd)
    if err != nil {
        return ord, aphgrpc.HandleUpdateError(ctx, err)
    }
    if mord.NotFound {
        return ord, aphgrpc.HandleNotFoundError(ctx, err)
    }
    ord.Data = s.orderData(mord)
    s.publisher.Publish(s.Topics["orderUpdate"], ord) //nolint

    return ord, nil
}

func (s *OrderService) orderQueryWithFilter(
    ctx context.Context,
    params *order.ListParameters,
) (*order.OrderCollection, error) {
    orc := &order.OrderCollection{}
    pfs, err := query.ParseFilterString(params.Filter)
    if err != nil {
        return orc, fmt.Errorf("error parsing filter string: %s", err)
    }
    str, err := query.GenAQLFilterStatement(&query.StatementParameters{
        Fmap:    arangodb.FMap,
        Filters: pfs,
        Doc:     "s",
    })
    if err != nil {
        return orc, fmt.Errorf("error generating AQL filter statement: %s", err)
    }
    // if the parsed statement is empty FILTER, just return empty string
    if str == "FILTER " {
        str = ""
    }
    mlo, err := s.repo.ListOrders(&order.ListParameters{
        Cursor: params.Cursor,
        Limit:  params.Limit,
        Filter: str,
    })
    if err != nil {
        return orc, aphgrpc.HandleGetError(ctx, err)
    }
    if len(mlo) == 0 {
        return orc, aphgrpc.HandleNotFoundError(ctx, err)
    }
    ocdata := make([]*order.OrderCollection_Data, 0)
    for _, item := range mlo {
        ocdata = append(ocdata, &order.OrderCollection_Data{
            Type: s.GetResourceName(),
            Id:   item.Key,
            Attributes: &order.OrderAttributes{
                CreatedAt:        aphgrpc.TimestampProto(item.CreatedAt),
                UpdatedAt:        aphgrpc.TimestampProto(item.UpdatedAt),
                Courier:          item.Courier,
                CourierAccount:   item.CourierAccount,
                Comments:         item.Comments,
                Payment:          item.Payment,
                PurchaseOrderNum: item.PurchaseOrderNum,
                Status:           statusToEnum(item.Status),
                Consumer:         item.Consumer,
                Payer:            item.Payer,
                Purchaser:        item.Purchaser,
                Items:            item.Items,
            },
        })
    }
    if len(ocdata) < int(params.Limit)-2 { // fewer results than limit
        orc.Data = ocdata
        orc.Meta = &order.Meta{Limit: params.Limit, Total: int64(len(ocdata))}

        return orc, nil
    }
    orc.Data = ocdata[:len(ocdata)-1]
    orc.Meta = &order.Meta{
        Limit:      params.Limit,
        NextCursor: genNextCursorVal(mlo[len(mlo)-1].CreatedAt),
        Total:      int64(len(ocdata)),
    }

    return orc, nil
}

func (s *OrderService) orderQueryWithoutFilter(
    ctx context.Context,
    params *order.ListParameters,
) (*order.OrderCollection, error) {
    orc := &order.OrderCollection{}
    mlo, err := s.repo.ListOrders(&order.ListParameters{
        Cursor: params.Cursor,
        Limit:  params.Limit,
    })
    if err != nil {
        return orc, aphgrpc.HandleGetError(ctx, err)
    }
    if len(mlo) == 0 {
        return orc, aphgrpc.HandleNotFoundError(ctx, err)
    }
    ocdata := make([]*order.OrderCollection_Data, 0)
    for _, item := range mlo {
        ocdata = append(ocdata, &order.OrderCollection_Data{
            Type: s.GetResourceName(),
            Id:   item.Key,
            Attributes: &order.OrderAttributes{
                CreatedAt:        aphgrpc.TimestampProto(item.CreatedAt),
                UpdatedAt:        aphgrpc.TimestampProto(item.UpdatedAt),
                Courier:          item.Courier,
                CourierAccount:   item.CourierAccount,
                Comments:         item.Comments,
                Payment:          item.Payment,
                PurchaseOrderNum: item.PurchaseOrderNum,
                Status:           statusToEnum(item.Status),
                Consumer:         item.Consumer,
                Payer:            item.Payer,
                Purchaser:        item.Purchaser,
                Items:            item.Items,
            },
        })
    }
    if len(ocdata) < int(params.Limit)-2 { // fewer results than limit
        orc.Data = ocdata
        orc.Meta = &order.Meta{Limit: params.Limit, Total: int64(len(ocdata))}

        return orc, nil
    }
    orc.Data = ocdata[:len(ocdata)-1]
    orc.Meta = &order.Meta{
        Limit:      params.Limit,
        NextCursor: genNextCursorVal(mlo[len(mlo)-1].CreatedAt),
        Total:      int64(len(ocdata)),
    }

    return orc, nil
}

// ListOrders lists all existing orders.
func (s *OrderService) ListOrders(
    ctx context.Context,
    params *order.ListParameters,
) (*order.OrderCollection, error) {
    orc := &order.OrderCollection{}
    lmt := params.Limit
    if params.Limit == 0 {
        lmt = 10
    }
    if len(params.Filter) > 0 {
        orc, err := s.orderQueryWithFilter(ctx, &order.ListParameters{
            Limit:  lmt,
            Cursor: params.Cursor,
            Filter: params.Filter,
        })
        if err != nil {
            return orc, err
        }
    } else {
        orc, err := s.orderQueryWithoutFilter(ctx, &order.ListParameters{
            Limit:  lmt,
            Cursor: params.Cursor,
        })
        if err != nil {
            return orc, err
        }
    }

    return orc, nil
}

// LoadOrder handles the loading of an existing order.
func (s *OrderService) LoadOrder(
    ctx context.Context,
    rxo *order.ExistingOrder,
) (*order.Order, error) {
    ord := &order.Order{}
    if err := rxo.Validate(); err != nil {
        return ord, aphgrpc.HandleInvalidParamError(ctx, err)
    }
    mlrd, err := s.repo.LoadOrder(rxo)
    if err != nil {
        return ord, aphgrpc.HandleInsertError(ctx, err)
    }
    if mlrd.NotFound {
        return ord, aphgrpc.HandleNotFoundError(ctx, err)
    }
    ord.Data = s.orderData(mlrd)
    s.publisher.Publish(s.Topics["orderCreate"], ord) //nolint:errcheck

    return ord, nil
}

// PrepareForOrder clears the database to prepare for loading data.
func (s *OrderService) PrepareForOrder(
    ctx context.Context,
    rmt *empty.Empty,
) (*empty.Empty, error) {
    e := &empty.Empty{}
    if err := s.repo.ClearOrders(); err != nil {
        return e, aphgrpc.HandleGenericError(ctx, err)
    }

    return e, nil
}

func (s *OrderService) orderData(ord *model.OrderDoc) *order.Order_Data {
    return &order.Order_Data{
        Type: s.GetResourceName(),
        Id:   ord.Key,
        Attributes: &order.OrderAttributes{
            CreatedAt:        aphgrpc.TimestampProto(ord.CreatedAt),
            UpdatedAt:        aphgrpc.TimestampProto(ord.UpdatedAt),
            Courier:          ord.Courier,
            CourierAccount:   ord.CourierAccount,
            Comments:         ord.Comments,
            Payment:          ord.Payment,
            PurchaseOrderNum: ord.PurchaseOrderNum,
            Status:           statusToEnum(ord.Status),
            Consumer:         ord.Consumer,
            Payer:            ord.Payer,
            Purchaser:        ord.Purchaser,
            Items:            ord.Items,
        },
    }
}

// genNextCursorVal converts to epoch(https://en.wikipedia.org/wiki/Unix_time)
// in milliseconds.
func genNextCursorVal(t time.Time) int64 {
    return t.UnixNano() / Divider
}

func statusToEnum(status string) order.OrderStatus {
    switch status {
    case "Shipped":
        return order.OrderStatus_SHIPPED
    case "Cancelled":
        return order.OrderStatus_CANCELLED
    case "Growing":
        return order.OrderStatus_GROWING
    default:
        break
    }

    return order.OrderStatus_IN_PREPARATION
}