feat: add limit owner
This commit is contained in:
@@ -266,16 +266,27 @@ type ProductAnalyticsPerParentCategoryResponse struct {
|
||||
}
|
||||
|
||||
type ProductAnalyticsPerParentCategoryData struct {
|
||||
ParentCategoryID uuid.UUID `json:"parent_category_id"`
|
||||
ParentCategoryName string `json:"parent_category_name"`
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalQuantity int64 `json:"total_quantity"`
|
||||
CategoryCount int64 `json:"category_count"`
|
||||
ProductCount int64 `json:"product_count"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||
ParentCategoryID uuid.UUID `json:"parent_category_id"`
|
||||
ParentCategoryName string `json:"parent_category_name"`
|
||||
OwnerFeePercent float64 `json:"owner_fee_percent"`
|
||||
SDL float64 `json:"sdl"`
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalQuantity int64 `json:"total_quantity"`
|
||||
CategoryCount int64 `json:"category_count"`
|
||||
ProductCount int64 `json:"product_count"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||
TopProduct *ParentCategoryTopProduct `json:"top_product"`
|
||||
}
|
||||
|
||||
// ParentCategoryTopProduct is the best-selling product of a parent category by revenue.
|
||||
type ParentCategoryTopProduct struct {
|
||||
ProductID uuid.UUID `json:"product_id"`
|
||||
ProductName string `json:"product_name"`
|
||||
QuantitySold int64 `json:"quantity_sold"`
|
||||
Revenue float64 `json:"revenue"`
|
||||
}
|
||||
|
||||
// ParentCategoryAnalyticsDetailRequest represents the request for the drill-down of one parent category
|
||||
@@ -302,14 +313,17 @@ type ParentCategoryAnalyticsDetailResponse struct {
|
||||
}
|
||||
|
||||
type ParentCategoryAnalyticsDetailSummary struct {
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalQuantity int64 `json:"total_quantity"`
|
||||
CategoryCount int64 `json:"category_count"`
|
||||
ProductCount int64 `json:"product_count"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||
OwnerFeePercent float64 `json:"owner_fee_percent"`
|
||||
SDL float64 `json:"sdl"`
|
||||
TotalRevenue float64 `json:"total_revenue"`
|
||||
TotalQuantity int64 `json:"total_quantity"`
|
||||
CategoryCount int64 `json:"category_count"`
|
||||
ProductCount int64 `json:"product_count"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
TotalStandardHpp float64 `json:"total_standard_hpp"`
|
||||
TotalFifoHpp float64 `json:"total_fifo_hpp"`
|
||||
TotalMovingAverageHpp float64 `json:"total_moving_average_hpp"`
|
||||
TopProduct *ParentCategoryTopProduct `json:"top_product"`
|
||||
}
|
||||
|
||||
type ParentCategoryAnalyticsDetailData struct {
|
||||
@@ -366,7 +380,7 @@ type BudgetPeriod struct {
|
||||
Revenue float64 `json:"revenue"`
|
||||
OrderCount int64 `json:"order_count"`
|
||||
LimitPurchase float64 `json:"limit_purchase"`
|
||||
LimitOwner float64 `json:"limit_owner"`
|
||||
SDL float64 `json:"sdl"`
|
||||
LimitTeam float64 `json:"limit_team"`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,29 +1,79 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type CreateCategoryRequest struct {
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
Name string `json:"name" validate:"required,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
OwnerFeePercent *float64 `json:"owner_fee_percent,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateCategoryRequest struct {
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
Name *string `json:"name,omitempty" validate:"omitempty,min=1,max=255"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
BusinessType *string `json:"business_type,omitempty"`
|
||||
OutletID *uuid.UUID `json:"outlet_id,omitempty"`
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
Order *int `json:"order,omitempty"`
|
||||
OwnerFeePercent *float64 `json:"owner_fee_percent,omitempty"`
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
|
||||
// Set when the field is sent as null (or "" for parent_id), which asks for the
|
||||
// value to be removed. A field that is left out stays unchanged.
|
||||
ClearParentID bool `json:"-"`
|
||||
ClearOwnerFeePercent bool `json:"-"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON tells an explicit null apart from a field that was left out, so a
|
||||
// category can be detached from its parent and an owner fee override can be removed.
|
||||
func (r *UpdateCategoryRequest) UnmarshalJSON(data []byte) error {
|
||||
var raw map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
isEmpty := func(key string, allowEmptyString bool) bool {
|
||||
value, ok := raw[key]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
value = bytes.TrimSpace(value)
|
||||
return bytes.Equal(value, []byte("null")) || (allowEmptyString && bytes.Equal(value, []byte(`""`)))
|
||||
}
|
||||
|
||||
clearParentID := isEmpty("parent_id", true)
|
||||
clearOwnerFeePercent := isEmpty("owner_fee_percent", false)
|
||||
if clearParentID {
|
||||
// An empty string is not a valid UUID, so keep it away from the decoder
|
||||
delete(raw, "parent_id")
|
||||
cleaned, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data = cleaned
|
||||
}
|
||||
|
||||
type plain UpdateCategoryRequest
|
||||
var decoded plain
|
||||
if err := json.Unmarshal(data, &decoded); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*r = UpdateCategoryRequest(decoded)
|
||||
r.ClearParentID = clearParentID
|
||||
r.ClearOwnerFeePercent = clearOwnerFeePercent
|
||||
return nil
|
||||
}
|
||||
|
||||
type ListCategoriesRequest struct {
|
||||
@@ -39,18 +89,19 @@ type ListCategoriesRequest struct {
|
||||
|
||||
// Category Response DTOs
|
||||
type CategoryResponse struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
ParentName *string `json:"parent_name,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
BusinessType string `json:"business_type"`
|
||||
Order int `json:"order"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uuid.UUID `json:"id"`
|
||||
OrganizationID uuid.UUID `json:"organization_id"`
|
||||
OutletID *uuid.UUID `json:"outlet_id"`
|
||||
ParentID *uuid.UUID `json:"parent_id,omitempty"`
|
||||
ParentName *string `json:"parent_name,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description"`
|
||||
BusinessType string `json:"business_type"`
|
||||
Order int `json:"order"`
|
||||
OwnerFeePercent *float64 `json:"owner_fee_percent"`
|
||||
Metadata map[string]interface{} `json:"metadata"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type ListCategoriesResponse struct {
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestUpdateCategoryRequestTellsNullFromOmitted(t *testing.T) {
|
||||
parentID := uuid.New()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
body string
|
||||
wantParentID *uuid.UUID
|
||||
wantClear bool
|
||||
wantClearFee bool
|
||||
wantFee *float64
|
||||
wantNameIsSet bool
|
||||
}{
|
||||
{name: "omitted leaves parent unchanged", body: `{"name":"Food"}`, wantNameIsSet: true},
|
||||
{name: "null detaches the parent", body: `{"parent_id":null}`, wantClear: true},
|
||||
{name: "empty string detaches the parent", body: `{"name":"Food","parent_id":""}`, wantClear: true, wantNameIsSet: true},
|
||||
{name: "uuid sets the parent", body: `{"parent_id":"` + parentID.String() + `"}`, wantParentID: &parentID},
|
||||
{name: "null removes the owner fee override", body: `{"owner_fee_percent":null}`, wantClearFee: true},
|
||||
{name: "number sets the owner fee", body: `{"owner_fee_percent":35}`, wantFee: func() *float64 { v := 35.0; return &v }()},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
var req UpdateCategoryRequest
|
||||
require.NoError(t, json.Unmarshal([]byte(tt.body), &req))
|
||||
|
||||
require.Equal(t, tt.wantParentID, req.ParentID)
|
||||
require.Equal(t, tt.wantClear, req.ClearParentID)
|
||||
require.Equal(t, tt.wantClearFee, req.ClearOwnerFeePercent)
|
||||
require.Equal(t, tt.wantFee, req.OwnerFeePercent)
|
||||
require.Equal(t, tt.wantNameIsSet, req.Name != nil)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCategoryRequestRejectsInvalidParentID(t *testing.T) {
|
||||
var req UpdateCategoryRequest
|
||||
require.Error(t, json.Unmarshal([]byte(`{"parent_id":"not-a-uuid"}`), &req))
|
||||
}
|
||||
Reference in New Issue
Block a user