Permalink
Browse files

List applied discounts for every line item

Whenever there is a discount applied to any line item it will be
listed in the DiscountItems slice in the calculation details of
a line item.
  • Loading branch information...
mraerino committed Dec 12, 2018
1 parent 76d54de commit 57a2a3941d2bfaab14e68944f82f49c654c4a1e1
Showing with 200 additions and 3 deletions.
  1. +84 −0 api/order_test.go
  2. +17 −0 api/utils_test.go
  3. +23 −2 calculator/calculator.go
  4. +52 −0 calculator/discount_type.go
  5. +17 −1 models/line_item.go
  6. +7 −0 models/order.go
@@ -15,6 +15,7 @@ import (
"github.com/jinzhu/gorm"
"github.com/stretchr/testify/assert"

"github.com/netlify/gocommerce/calculator"
"github.com/netlify/gocommerce/claims"
"github.com/netlify/gocommerce/models"
"github.com/stretchr/testify/require"
@@ -165,6 +166,89 @@ func TestOrderCreate(t *testing.T) {
assert.Equal(t, total, order.Total, fmt.Sprintf("Total should be 1105, was %v", order.Total))
assert.Equal(t, taxes, order.Taxes, fmt.Sprintf("Total should be 106, was %v", order.Taxes))
})

t.Run("WithCoupon", func(t *testing.T) {
test := NewRouteTest(t)
test.Config.SiteURL = server.URL

couponServer := startCouponList("SPECIAL-EVENT", 10)
defer couponServer.Close()
test.Config.Coupons.URL = couponServer.URL

body := strings.NewReader(`{
"email": "info@example.com",
"shipping_address": {
"name": "Test User",
"address1": "610 22nd Street",
"city": "San Francisco", "state": "CA", "country": "USA", "zip": "94107"
},
"line_items": [{"path": "/simple-product", "quantity": 1}],
"coupon": "SPECIAL-EVENT"
}`)
token := test.Data.testUserToken
recorder := test.TestEndpoint(http.MethodPost, "/orders", body, token)

order := &models.Order{}
extractPayload(t, http.StatusCreated, recorder, order)
var total uint64 = 899
var discount uint64 = 100
assert.Equal(t, "info@example.com", order.Email, "Email should be info@example.com, was %v", order.Email)
assert.Equal(t, total, order.Total, fmt.Sprintf("Total should be 899, was %v", order.Total))
assert.Equal(t, discount, order.Discount, fmt.Sprintf("Discount should be 100, was %v", order.Total))
assert.Len(t, order.LineItems, 1)

lineItem := order.LineItems[0]
assert.Equal(t, int64(total), lineItem.CalculationDetail.Total, fmt.Sprintf("Total should be 899, was %d", lineItem.CalculationDetail.Total))
assert.Equal(t, discount, lineItem.CalculationDetail.Discount, fmt.Sprintf("Discount should be 100, was %d", lineItem.CalculationDetail.Discount))
assert.Len(t, lineItem.CalculationDetail.DiscountItems, 1)

discountItem := lineItem.CalculationDetail.DiscountItems[0]
assert.Equal(t, calculator.DiscountTypeCoupon, discountItem.Type)
assert.Equal(t, uint64(10), discountItem.Percentage)
assert.Equal(t, uint64(0), discountItem.Fixed)
})

t.Run("WithMemberDiscount", func(t *testing.T) {
test := NewRouteTest(t)

settings := calculator.Settings{
MemberDiscounts: []*calculator.MemberDiscount{
&calculator.MemberDiscount{
Claims: map[string]string{
"email": test.Data.testUser.Email,
},
Percentage: 15,
ProductTypes: []string{"Book"},
},
},
}
server := startTestSiteWithSettings(settings)
defer server.Close()
test.Config.SiteURL = server.URL

body := strings.NewReader(defaultPayload)
token := test.Data.testUserToken
recorder := test.TestEndpoint(http.MethodPost, "/orders", body, token)

order := &models.Order{}
extractPayload(t, http.StatusCreated, recorder, order)
var total uint64 = 849
var discount uint64 = 150
assert.Equal(t, "info@example.com", order.Email, "Email should be info@example.com, was %v", order.Email)
assert.Equal(t, total, order.Total, fmt.Sprintf("Total should be 849, was %v", order.Total))
assert.Equal(t, discount, order.Discount, fmt.Sprintf("Discount should be 150, was %v", order.Total))
assert.Len(t, order.LineItems, 1)

lineItem := order.LineItems[0]
assert.Equal(t, int64(total), lineItem.CalculationDetail.Total, fmt.Sprintf("Total should be 849, was %d", lineItem.CalculationDetail.Total))
assert.Equal(t, discount, lineItem.CalculationDetail.Discount, fmt.Sprintf("Discount should be 150, was %d", lineItem.CalculationDetail.Discount))
assert.Len(t, lineItem.CalculationDetail.DiscountItems, 1)

discountItem := lineItem.CalculationDetail.DiscountItems[0]
assert.Equal(t, calculator.DiscountTypeMember, discountItem.Type)
assert.Equal(t, uint64(15), discountItem.Percentage)
assert.Equal(t, uint64(0), discountItem.Fixed)
})
}

func TestOrderCreateNewUser(t *testing.T) {
@@ -481,3 +481,20 @@ func startTestSiteWithSettings(settings interface{}) *httptest.Server {
}
}))
}

func startCouponList(name string, percentage uint64) *httptest.Server {
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/":
fmt.Fprintf(w, `{
"coupons": {
"%s": {
"percentage": %d
}
}
}\n`, name, percentage)
default:
w.WriteHeader(http.StatusNotFound)
}
}))
}
@@ -8,6 +8,13 @@ import (
"github.com/sirupsen/logrus"
)

// DiscountItem provides details about a discount that was applied
type DiscountItem struct {
Type DiscountType `json:"type"`
Percentage uint64 `json:"percentage"`
Fixed uint64 `json:"fixed"`
}

// Price represents the total price of all line items.
type Price struct {
Items []ItemPrice
@@ -28,6 +35,8 @@ type ItemPrice struct {
NetTotal uint64
Taxes uint64
Total int64

DiscountItems []DiscountItem
}

// PaymentMethods settings
@@ -182,14 +191,26 @@ func calculateAmountsForSingleItem(settings *Settings, lineLogger logrus.FieldLo
// apply discount to original price
coupon := params.Coupon
if coupon != nil && coupon.ValidForType(item.ProductType()) && coupon.ValidForProduct(item.ProductSku()) {
itemPrice.Discount = calculateDiscount(singlePrice, coupon.PercentageDiscount(), coupon.FixedDiscount(params.Currency)*multiplier)
discountItem := DiscountItem{
Type: DiscountTypeCoupon,
Percentage: coupon.PercentageDiscount(),
Fixed: coupon.FixedDiscount(params.Currency) * multiplier,
}
itemPrice.Discount = calculateDiscount(singlePrice, discountItem.Percentage, discountItem.Fixed)
itemPrice.DiscountItems = append(itemPrice.DiscountItems, discountItem)
}
if settings != nil && settings.MemberDiscounts != nil {
for _, discount := range settings.MemberDiscounts {

if jwtClaims != nil && claims.HasClaims(jwtClaims, discount.Claims) && discount.ValidForType(item.ProductType()) && discount.ValidForProduct(item.ProductSku()) {
lineLogger = lineLogger.WithField("discount", discount.Claims)
itemPrice.Discount += calculateDiscount(singlePrice, discount.Percentage, discount.FixedDiscount(params.Currency)*multiplier)
discountItem := DiscountItem{
Type: DiscountTypeMember,
Percentage: discount.Percentage,
Fixed: discount.FixedDiscount(params.Currency) * multiplier,
}
itemPrice.Discount += calculateDiscount(singlePrice, discountItem.Percentage, discountItem.Fixed)
itemPrice.DiscountItems = append(itemPrice.DiscountItems, discountItem)
}
}
}
@@ -0,0 +1,52 @@
package calculator

import (
"bytes"
"encoding/json"
)

// DiscountType indicates what type of discount was given
type DiscountType int

// possible types for a discount item
const (
DiscountTypeCoupon DiscountType = iota + 1
DiscountTypeMember
)

func (t DiscountType) String() string {
switch t {
case DiscountTypeCoupon:
return "coupon"
case DiscountTypeMember:
return "member"
}
return "unknown"
}

// MarshalJSON marshals the enum as a quoted json string
func (t DiscountType) MarshalJSON() ([]byte, error) {
buffer := bytes.NewBufferString(`"`)
buffer.WriteString(t.String())
buffer.WriteString(`"`)
return buffer.Bytes(), nil
}

// UnmarshalJSON unmashals a quoted json string to the enum value
func (t *DiscountType) UnmarshalJSON(b []byte) error {
var j string
err := json.Unmarshal(b, &j)
if err != nil {
return err
}

switch j {
case "coupon":
*t = DiscountTypeCoupon
case "member":
*t = DiscountTypeMember
default:
*t = 0
}
return nil
}
@@ -13,10 +13,26 @@ import (
"github.com/pborman/uuid"
)

// DiscountItem provides details about a discount that was applied
type DiscountItem struct {
ID int64 `json:"-"`
LineItemID int64 `json:"-"`

calculator.DiscountItem `gorm:"embedded"`
}

// TableName returns the database table name for the DiscountItem model.
func (DiscountItem) TableName() string {
return tableName("discount_items")
}

// CalculationDetail holds details about pricing for line items
type CalculationDetail struct {
Subtotal uint64 `json:"subtotal"`
Discount uint64 `json:"discount"`

Discount uint64 `json:"discount"`
DiscountItems []DiscountItem `json:"discount_items" gorm:"foreignkey:LineItemID"`

NetTotal uint64 `json:"net_total"`
Taxes uint64 `json:"taxes"`
Total int64 `json:"total"`
@@ -188,6 +188,13 @@ func (o *Order) CalculateTotal(settings *calculator.Settings, claims map[string]
Taxes: item.Taxes,
Total: item.Total,
}

for _, discount := range item.DiscountItems {
discount := DiscountItem{
DiscountItem: discount,
}
o.LineItems[i].CalculationDetail.DiscountItems = append(o.LineItems[i].CalculationDetail.DiscountItems, discount)
}
}

if price.Total > 0 {

0 comments on commit 57a2a39

Please sign in to comment.