flamingo.me/flamingo-commerce/v3@v3.11.0/price/domain/price.go (about) 1 package domain 2 3 import ( 4 "encoding" 5 "encoding/json" 6 "errors" 7 "math" 8 "math/big" 9 "strconv" 10 11 "github.com/Rhymond/go-money" 12 ) 13 14 type ( 15 // Price is a Type that represents a Amount - it is immutable 16 // DevHint: We use Amount and Charge as Value - so we do not pass pointers. (According to Go Wiki's code review comments page suggests passing by value when structs are small and likely to stay that way) 17 Price struct { 18 amount big.Float `swaggertype:"string"` 19 currency string 20 } 21 22 // Charge is a Amount of a certain Type. Charge is used as value object 23 Charge struct { 24 // Price that is paid, can be in a certain currency 25 Price Price 26 // Value of the "Price" in another (base) currency 27 Value Price 28 // Type of the charge - can be ChargeTypeMain or something else. Used to differentiate between different charges of a single thing 29 Type string 30 // Reference contains further information to distinguish charges of the same type 31 Reference string 32 } 33 34 // Charges - Represents the Charges the product need to be paid with 35 Charges struct { 36 chargesByQualifier map[ChargeQualifier]Charge 37 } 38 39 // ChargeQualifier distinguishes charges by type and reference 40 ChargeQualifier struct { 41 // Type represents charge type 42 Type string 43 // Reference contains further information to distinguish charges of the same type 44 Reference string 45 } 46 47 // priceEncodeAble is a type that we need to allow marshalling the price values. The type itself is unexported 48 priceEncodeAble struct { 49 Amount big.Float 50 Currency string 51 } 52 ) 53 54 var ( 55 _ encoding.BinaryMarshaler = Price{} 56 _ encoding.BinaryUnmarshaler = &Price{} 57 ) 58 59 const ( 60 // ChargeTypeGiftCard used as a charge type for gift cards 61 ChargeTypeGiftCard = "giftcard" 62 // ChargeTypeMain used as default for a Charge 63 ChargeTypeMain = "main" 64 65 // RoundingModeFloor use if you want to cut (round down) 66 RoundingModeFloor = "floor" 67 // RoundingModeCeil use if you want to round up always 68 RoundingModeCeil = "ceil" 69 // RoundingModeHalfUp round up if the discarded fraction is ≥ 0.5, otherwise round down. Default for GetPayable() 70 RoundingModeHalfUp = "halfup" 71 // RoundingModeHalfDown round up if the discarded fraction is > 0.5, otherwise round down. 72 RoundingModeHalfDown = "halfdown" 73 ) 74 75 // NewFromFloat - factory method 76 func NewFromFloat(amount float64, currency string) Price { 77 return Price{ 78 amount: *big.NewFloat(amount), 79 currency: currency, 80 } 81 } 82 83 // NewFromBigFloat - factory method 84 func NewFromBigFloat(amount big.Float, currency string) Price { 85 return Price{ 86 amount: amount, 87 currency: currency, 88 } 89 } 90 91 // NewZero Zero price 92 func NewZero(currency string) Price { 93 return Price{ 94 amount: *new(big.Float).SetInt64(0), 95 currency: currency, 96 } 97 } 98 99 // NewFromInt use to set money by smallest payable unit - e.g. to set 2.45 EUR you should use NewFromInt(245, 100, "EUR") 100 func NewFromInt(amount int64, precision int, currency string) Price { 101 amountF := new(big.Float).SetInt64(amount) 102 if precision == 0 { 103 return Price{ 104 amount: *new(big.Float).SetInt64(0), 105 currency: currency, 106 } 107 } 108 precicionF := new(big.Float).SetInt64(int64(precision)) 109 return Price{ 110 amount: *new(big.Float).Quo(amountF, precicionF), 111 currency: currency, 112 } 113 } 114 115 // Add the given price to the current price and returns a new price 116 func (p Price) Add(add Price) (Price, error) { 117 newPrice, err := p.currencyGuard(add) 118 if err != nil { 119 return newPrice, err 120 } 121 newPrice.amount.Add(&p.amount, &add.amount) 122 return newPrice, nil 123 } 124 125 // ForceAdd tries to add the given price to the current price - will not return errors 126 func (p Price) ForceAdd(add Price) Price { 127 newPrice, err := p.currencyGuard(add) 128 if err != nil { 129 return p 130 } 131 newPrice.amount.Add(&p.amount, &add.amount) 132 return newPrice 133 } 134 135 // currencyGuard is a common Guard that protects price calculations of prices with different currency. 136 // Robust: if original is Zero and the currencies are different we take the given currency 137 func (p Price) currencyGuard(check Price) (Price, error) { 138 if p.currency == check.currency { 139 return Price{ 140 currency: check.currency, 141 }, nil 142 } 143 if p.IsZero() { 144 return Price{ 145 currency: check.currency, 146 }, nil 147 } 148 149 if check.IsZero() { 150 return Price{ 151 currency: p.currency, 152 }, nil 153 } 154 return NewZero(p.currency), errors.New("cannot calculate prices in different currencies") 155 } 156 157 // Discounted returns new price reduced by given percent 158 func (p Price) Discounted(percent float64) Price { 159 newPrice := Price{ 160 currency: p.currency, 161 amount: *new(big.Float).Mul(&p.amount, big.NewFloat((100-percent)/100)), 162 } 163 return newPrice 164 } 165 166 // Taxed returns new price added with Tax (assuming current price is net) 167 func (p Price) Taxed(percent big.Float) Price { 168 newPrice := Price{ 169 currency: p.currency, 170 amount: *new(big.Float).Add(&p.amount, p.TaxFromNet(percent).Amount()), 171 } 172 return newPrice 173 } 174 175 // TaxFromNet returns new price representing the tax amount (assuming the current price is net 100%) 176 func (p Price) TaxFromNet(percent big.Float) Price { 177 quo := new(big.Float).Mul(&percent, &p.amount) 178 newPrice := Price{ 179 currency: p.currency, 180 amount: *new(big.Float).Quo(quo, new(big.Float).SetInt64(100)), 181 } 182 return newPrice 183 } 184 185 // TaxFromGross returns new price representing the tax amount (assuming the current price is gross 100+percent) 186 func (p Price) TaxFromGross(percent big.Float) Price { 187 quo := new(big.Float).Mul(&percent, &p.amount) 188 percent100 := new(big.Float).Add(&percent, new(big.Float).SetInt64(100)) 189 newPrice := Price{ 190 currency: p.currency, 191 amount: *new(big.Float).Quo(quo, percent100), 192 } 193 return newPrice 194 } 195 196 // Sub the given price from the current price and returns a new price 197 func (p Price) Sub(sub Price) (Price, error) { 198 newPrice, err := p.currencyGuard(sub) 199 if err != nil { 200 return newPrice, err 201 } 202 newPrice.amount.Sub(&p.amount, &sub.amount) 203 return newPrice, nil 204 } 205 206 // Inverse returns the price multiplied with -1 207 func (p Price) Inverse() Price { 208 p.amount = *new(big.Float).Mul(&p.amount, big.NewFloat(-1)) 209 return p 210 } 211 212 // Multiply returns a new price with the amount Multiply 213 func (p Price) Multiply(qty int) Price { 214 newPrice := Price{ 215 currency: p.currency, 216 } 217 newPrice.amount.Mul(&p.amount, new(big.Float).SetInt64(int64(qty))) 218 return newPrice 219 } 220 221 // Divided returns a new price with the amount Divided 222 func (p Price) Divided(qty int) Price { 223 newPrice := Price{ 224 currency: p.currency, 225 } 226 if qty == 0 { 227 return NewZero(p.currency) 228 } 229 newPrice.amount.Quo(&p.amount, new(big.Float).SetInt64(int64(qty))) 230 return newPrice 231 } 232 233 // Equal compares the prices exact 234 func (p Price) Equal(cmp Price) bool { 235 if p.currency != cmp.currency { 236 return false 237 } 238 return p.amount.Cmp(&cmp.amount) == 0 239 } 240 241 // LikelyEqual compares the prices with some tolerance 242 func (p Price) LikelyEqual(cmp Price) bool { 243 if p.currency != cmp.currency { 244 return false 245 } 246 diff := new(big.Float).Sub(&p.amount, &cmp.amount) 247 absDiff := new(big.Float).Abs(diff) 248 return absDiff.Cmp(big.NewFloat(0.000000001)) == -1 249 } 250 251 // IsLessThen compares the current price with a given one 252 func (p Price) IsLessThen(cmp Price) bool { 253 if p.currency != cmp.currency { 254 return false 255 } 256 return p.amount.Cmp(&cmp.amount) == -1 257 } 258 259 // IsGreaterThen compares the current price with a given one 260 func (p Price) IsGreaterThen(cmp Price) bool { 261 if p.currency != cmp.currency { 262 return false 263 } 264 return p.amount.Cmp(&cmp.amount) == 1 265 } 266 267 // IsLessThenValue compares the price with a given amount value (assuming same currency) 268 func (p Price) IsLessThenValue(amount big.Float) bool { 269 return p.amount.Cmp(&amount) == -1 270 } 271 272 // IsGreaterThenValue compares the price with a given amount value (assuming same currency) 273 func (p Price) IsGreaterThenValue(amount big.Float) bool { 274 return p.amount.Cmp(&amount) == 1 275 } 276 277 // IsNegative returns true if the price represents a negative value 278 func (p Price) IsNegative() bool { 279 return p.IsLessThenValue(*big.NewFloat(0.0)) 280 } 281 282 // IsPositive returns true if the price represents a positive value 283 func (p Price) IsPositive() bool { 284 return p.IsGreaterThenValue(*big.NewFloat(0.0)) 285 } 286 287 // IsPayable returns true if the price represents a payable (rounded) value 288 func (p Price) IsPayable() bool { 289 return p.GetPayable().Equal(p) 290 } 291 292 // IsZero returns true if the price represents zero value 293 func (p Price) IsZero() bool { 294 return p.LikelyEqual(NewZero(p.Currency())) || p.LikelyEqual(NewFromFloat(0, p.Currency())) 295 } 296 297 // FloatAmount gets the current amount as float 298 func (p Price) FloatAmount() float64 { 299 a, _ := p.amount.Float64() 300 return a 301 } 302 303 // GetPayable rounds the price with the precision required by the currency in a price that can actually be paid 304 // e.g. an internal amount of 1,23344 will get rounded to 1,23 305 func (p Price) GetPayable() Price { 306 mode, precision := p.payableRoundingPrecision() 307 return p.GetPayableByRoundingMode(mode, precision) 308 } 309 310 // GetPayableByRoundingMode returns the price rounded you can pass the used rounding mode and precision 311 // Example for precision 100: 312 // 313 // 1.115 > 1.12 (RoundingModeHalfUp) / 1.11 (RoundingModeFloor) 314 // -1.115 > -1.11 (RoundingModeHalfUp) / -1.12 (RoundingModeFloor) 315 func (p Price) GetPayableByRoundingMode(mode string, precision int) Price { 316 newPrice := Price{ 317 currency: p.currency, 318 } 319 320 amountForRound := new(big.Float).Copy(&p.amount) 321 negative := int64(1) 322 if p.IsNegative() { 323 negative = -1 324 } 325 326 amountTruncatedFloat, _ := new(big.Float).Mul(amountForRound, p.precisionF(precision)).Float64() 327 integerPart, fractionalPart := math.Modf(amountTruncatedFloat) 328 amountTruncatedInt := int64(integerPart) 329 valueAfterPrecision := (math.Round(fractionalPart*1000) / 100) * float64(negative) 330 if amountTruncatedFloat >= float64(math.MaxInt64) { 331 // will not work if we are already above MaxInt - so we return unrounded price: 332 newPrice.amount = p.amount 333 return newPrice 334 } 335 336 switch mode { 337 case RoundingModeCeil: 338 if negative == 1 && valueAfterPrecision > 0 { 339 amountTruncatedInt = amountTruncatedInt + negative 340 } 341 case RoundingModeHalfUp: 342 if valueAfterPrecision >= 5 { 343 amountTruncatedInt = amountTruncatedInt + negative 344 } 345 case RoundingModeHalfDown: 346 if valueAfterPrecision > 5 { 347 amountTruncatedInt = amountTruncatedInt + negative 348 } 349 case RoundingModeFloor: 350 if negative == -1 && valueAfterPrecision > 0 { 351 amountTruncatedInt = amountTruncatedInt + negative 352 } 353 default: 354 // nothing to round 355 } 356 357 amountRounded := new(big.Float).Quo(new(big.Float).SetInt64(amountTruncatedInt), p.precisionF(precision)) 358 newPrice.amount = *amountRounded 359 return newPrice 360 } 361 362 // precisionF returns big.Float from int 363 func (p Price) precisionF(precision int) *big.Float { 364 return new(big.Float).SetInt64(int64(precision)) 365 } 366 367 // precisionF - 10 * n - n is the amount of decimal numbers after comma 368 func (p Price) payableRoundingPrecision() (string, int) { 369 currency := money.GetCurrency(p.currency) 370 if currency == nil { 371 return RoundingModeFloor, 1 372 } 373 374 precision := 1 375 for i := 1; i <= currency.Fraction; i++ { 376 precision *= 10 377 } 378 379 return RoundingModeHalfUp, precision 380 } 381 382 // SplitInPayables returns "count" payable prices (each rounded) that in sum matches the given price 383 // - Given a price of 12.456 (Payable 12,46) - Splitted in 6 will mean: 6 * 2.076 384 // - but having them payable requires rounding them each (e.g. 2.07) which would mean we have 0.03 difference (=12,45-6*2.07) 385 // - so that the sum is as close as possible to the original value in this case the correct return will be: 386 // - 2.07 + 2.07+2.08 +2.08 +2.08 +2.08 387 func (p Price) SplitInPayables(count int) ([]Price, error) { 388 if count <= 0 { 389 return nil, errors.New("split must be higher than zero") 390 } 391 // guard clause invert negative values 392 _, precision := p.payableRoundingPrecision() 393 amount := p.GetPayable().Amount() 394 // we have to invert negative numbers, otherwise split is not correct 395 if p.IsNegative() { 396 amount = p.GetPayable().Inverse().Amount() 397 } 398 amountToMatchFloat, _ := new(big.Float).Mul(amount, p.precisionF(precision)).Float64() 399 amountToMatchInt := int64(amountToMatchFloat) 400 401 splittedAmountModulo := amountToMatchInt % int64(count) 402 splittedAmount := amountToMatchInt / int64(count) 403 404 splittedAmounts := make([]int64, count) 405 for i := 0; i < count; i++ { 406 splittedAmounts[i] = splittedAmount 407 } 408 409 for i := 0; i < int(splittedAmountModulo); i++ { 410 splittedAmounts[i] = splittedAmounts[i] + 1 411 } 412 413 prices := make([]Price, count) 414 for i := 0; i < count; i++ { 415 _, precision := p.payableRoundingPrecision() 416 splittedAmount := splittedAmounts[i] 417 // invert prices again to keep negative values 418 if p.IsNegative() { 419 splittedAmount *= -1 420 } 421 prices[i] = NewFromInt(splittedAmount, precision, p.Currency()) 422 } 423 424 return prices, nil 425 } 426 427 // Clone returns a copy of the price - the amount gets Excat acc 428 func (p Price) Clone() Price { 429 return Price{ 430 amount: *new(big.Float).Set(&p.amount), 431 currency: p.currency, 432 } 433 } 434 435 // Currency returns currency 436 func (p Price) Currency() string { 437 return p.currency 438 } 439 440 // Amount returns exact amount as bigFloat 441 func (p Price) Amount() *big.Float { 442 return &p.amount 443 } 444 445 // SumAll returns new price with sum of all given prices 446 func SumAll(prices ...Price) (Price, error) { 447 if len(prices) == 0 { 448 return NewZero(""), errors.New("no price given") 449 } 450 result := prices[0].Clone() 451 var err error 452 for _, price := range prices[1:] { 453 result, err = result.Add(price) 454 if err != nil { 455 return result, err 456 } 457 } 458 return result, nil 459 } 460 461 // MarshalJSON implements interface required by json marshal 462 func (p Price) MarshalJSON() (data []byte, err error) { 463 type priceJSON struct { 464 Amount string 465 Currency string 466 } 467 468 pn := priceJSON{ 469 Amount: strconv.FormatFloat(p.GetPayable().FloatAmount(), 'f', 2, 64), 470 Currency: p.currency, 471 } 472 473 r, e := json.Marshal(&pn) 474 return r, e 475 } 476 477 // MarshalBinary implements interface required by gob 478 func (p Price) MarshalBinary() (data []byte, err error) { 479 return json.Marshal(p) 480 } 481 482 // UnmarshalBinary implements interface required by gob. 483 // Modifies the receiver so it must take a pointer receiver! 484 func (p *Price) UnmarshalBinary(data []byte) error { 485 var pe priceEncodeAble 486 err := json.Unmarshal(data, &pe) 487 if err != nil { 488 return err 489 } 490 p.amount = pe.Amount 491 p.currency = pe.Currency 492 return nil 493 } 494 495 // UnmarshalJSON implements encode Unmarshaler 496 func (p *Price) UnmarshalJSON(data []byte) error { 497 return p.UnmarshalBinary(data) 498 } 499 500 // Add the given Charge to the current Charge and returns a new Charge 501 func (p Charge) Add(add Charge) (Charge, error) { 502 if p.Type != add.Type { 503 return Charge{}, errors.New("charge type mismatch") 504 } 505 newPrice, err := p.Price.Add(add.Price) 506 if err != nil { 507 return Charge{}, err 508 } 509 p.Price = newPrice 510 511 newPrice, err = p.Value.Add(add.Value) 512 if err != nil { 513 return Charge{}, err 514 } 515 p.Value = newPrice 516 return p, nil 517 } 518 519 // GetPayable rounds the charge 520 func (p Charge) GetPayable() Charge { 521 p.Value = p.Value.GetPayable() 522 p.Price = p.Price.GetPayable() 523 return p 524 } 525 526 // Mul the given Charge and returns a new Charge 527 func (p Charge) Mul(qty int) Charge { 528 p.Price = p.Price.Multiply(qty) 529 p.Value = p.Value.Multiply(qty) 530 return p 531 } 532 533 // NewCharges creates a new Charges object 534 func NewCharges(chargesByType map[string]Charge) *Charges { 535 charges := addChargeQualifier(chargesByType) 536 return &charges 537 } 538 539 // HasType returns a true if any charges include a charge with given type 540 func (c Charges) HasType(ctype string) bool { 541 for qualifier := range c.chargesByQualifier { 542 if qualifier.Type == ctype { 543 return true 544 } 545 } 546 return false 547 } 548 549 // GetByType returns a charge of given type. If it was not found a Zero amount 550 // is returned and the second return value is false 551 // sums up charges by a certain type if there are multiple 552 func (c Charges) GetByType(ctype string) (Charge, bool) { 553 // guard in case type is not available 554 if !c.HasType(ctype) { 555 return Charge{}, false 556 } 557 result := Charge{ 558 Type: ctype, 559 } 560 // sum up all charges with certain type to one charge 561 for qualifier, charge := range c.chargesByQualifier { 562 if qualifier.Type == ctype { 563 result, _ = result.Add(charge) 564 } 565 } 566 return result, true 567 } 568 569 // HasChargeQualifier returns a true if any charges include a charge with given type 570 // and concrete key values provided by additional 571 func (c Charges) HasChargeQualifier(qualifier ChargeQualifier) bool { 572 if _, ok := c.chargesByQualifier[qualifier]; ok { 573 return true 574 } 575 return false 576 } 577 578 // GetByChargeQualifier returns a charge of given qualifier. 579 // If it was not found a Zero amount is returned and the second return value is false 580 func (c Charges) GetByChargeQualifier(qualifier ChargeQualifier) (Charge, bool) { 581 // guard in case type is not available 582 if !c.HasChargeQualifier(qualifier) { 583 return Charge{}, false 584 } 585 586 if charge, ok := c.chargesByQualifier[qualifier]; ok { 587 return charge, true 588 } 589 return Charge{}, false 590 } 591 592 // GetByChargeQualifierForced returns a charge of given qualifier. 593 // If it was not found a Zero amount is returned. This method might be useful to call in View (template) directly. 594 func (c Charges) GetByChargeQualifierForced(qualifier ChargeQualifier) Charge { 595 result, ok := c.GetByChargeQualifier(qualifier) 596 if !ok { 597 return Charge{} 598 } 599 return result 600 } 601 602 // GetByTypeForced returns a charge of given type. If it was not found a Zero amount is returned. 603 // This method might be useful to call in View (template) directly where you need one return value 604 // sums up charges by a certain type if there are multiple 605 func (c Charges) GetByTypeForced(ctype string) Charge { 606 result, ok := c.GetByType(ctype) 607 if !ok { 608 return Charge{} 609 } 610 return result 611 } 612 613 // GetAllCharges returns all charges 614 func (c Charges) GetAllCharges() map[ChargeQualifier]Charge { 615 return c.chargesByQualifier 616 } 617 618 // GetAllByType returns all charges of type 619 func (c Charges) GetAllByType(ctype string) map[ChargeQualifier]Charge { 620 chargesByType := make(map[ChargeQualifier]Charge) 621 622 for qualifier, charge := range c.chargesByQualifier { 623 if qualifier.Type == ctype { 624 chargesByType[ChargeQualifier{ 625 qualifier.Type, 626 qualifier.Reference, 627 }] = charge 628 } 629 } 630 631 return chargesByType 632 } 633 634 // Add returns new Charges with the given added 635 func (c Charges) Add(toadd Charges) Charges { 636 if c.chargesByQualifier == nil { 637 c.chargesByQualifier = make(map[ChargeQualifier]Charge) 638 } 639 for addk, addCharge := range toadd.chargesByQualifier { 640 if existingCharge, ok := c.chargesByQualifier[addk]; ok { 641 chargeSum, _ := existingCharge.Add(addCharge) 642 c.chargesByQualifier[addk] = chargeSum.GetPayable() 643 } else { 644 c.chargesByQualifier[addk] = addCharge 645 } 646 } 647 return c 648 } 649 650 // AddCharge returns new Charges with the given Charge added 651 func (c Charges) AddCharge(toadd Charge) Charges { 652 if c.chargesByQualifier == nil { 653 c.chargesByQualifier = make(map[ChargeQualifier]Charge) 654 } 655 qualifier := ChargeQualifier{ 656 Type: toadd.Type, 657 Reference: toadd.Reference, 658 } 659 if existingCharge, ok := c.chargesByQualifier[qualifier]; ok { 660 chargeSum, _ := existingCharge.Add(toadd) 661 c.chargesByQualifier[qualifier] = chargeSum.GetPayable() 662 } else { 663 c.chargesByQualifier[qualifier] = toadd 664 } 665 666 return c 667 } 668 669 // Mul returns new Charges with the given multiplied 670 func (c Charges) Mul(qty int) Charges { 671 if c.chargesByQualifier == nil { 672 return c 673 } 674 for t, charge := range c.chargesByQualifier { 675 c.chargesByQualifier[t] = charge.Mul(qty) 676 } 677 return c 678 } 679 680 // Items returns all charges items 681 func (c Charges) Items() []Charge { 682 var charges []Charge 683 684 for _, charge := range c.chargesByQualifier { 685 charges = append(charges, charge) 686 } 687 688 return charges 689 } 690 691 // addChargeQualifier parse string keys to charge qualifier for backwards compatibility 692 func addChargeQualifier(chargesByType map[string]Charge) Charges { 693 withQualifier := make(map[ChargeQualifier]Charge) 694 for chargeType, charge := range chargesByType { 695 qualifier := ChargeQualifier{ 696 Type: chargeType, 697 Reference: charge.Reference, 698 } 699 withQualifier[qualifier] = charge 700 } 701 return Charges{chargesByQualifier: withQualifier} 702 }