update tests and discovery
This commit is contained in:
@@ -14,9 +14,6 @@ type CheckoutMeta struct {
|
|||||||
Terms string
|
Terms string
|
||||||
Checkout string
|
Checkout string
|
||||||
Confirmation string
|
Confirmation string
|
||||||
Notification string
|
|
||||||
Validation string
|
|
||||||
Push string
|
|
||||||
Country string
|
Country string
|
||||||
Currency string // optional override (defaults to "SEK" if empty)
|
Currency string // optional override (defaults to "SEK" if empty)
|
||||||
Locale string // optional override (defaults to "sv-se" if empty)
|
Locale string // optional override (defaults to "sv-se" if empty)
|
||||||
@@ -108,9 +105,9 @@ func BuildCheckoutOrderPayload(grain *cart.CartGrain, meta *CheckoutMeta) ([]byt
|
|||||||
Terms: meta.Terms,
|
Terms: meta.Terms,
|
||||||
Checkout: meta.Checkout,
|
Checkout: meta.Checkout,
|
||||||
Confirmation: meta.Confirmation,
|
Confirmation: meta.Confirmation,
|
||||||
Notification: meta.Notification,
|
Notification: "https://cart.tornberg.me/notification",
|
||||||
Validation: meta.Validation,
|
Validation: "https://cart.tornberg.me/validate",
|
||||||
Push: meta.Push,
|
Push: "https://cart.tornberg.me/push?order_id={checkout.order.id}",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -335,30 +335,27 @@ func getInventoryRequests(items []*cart.CartItem) []inventory.ReserveRequest {
|
|||||||
return requests
|
return requests
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PoolServer) CreateOrUpdateCheckout(ctx context.Context, host string, id cart.CartId) (*CheckoutOrder, error) {
|
func (s *PoolServer) CreateOrUpdateCheckout(r *http.Request, id cart.CartId) (*CheckoutOrder, error) {
|
||||||
country := getCountryFromHost(host)
|
country := getCountryFromHost(r.Host)
|
||||||
meta := &CheckoutMeta{
|
meta := &CheckoutMeta{
|
||||||
Terms: fmt.Sprintf("https://%s/terms", host),
|
Terms: fmt.Sprintf("https://%s/terms", r.Host),
|
||||||
Checkout: fmt.Sprintf("https://%s/checkout?order_id={checkout.order.id}", host),
|
Checkout: fmt.Sprintf("https://%s/checkout?order_id={checkout.order.id}", r.Host),
|
||||||
Confirmation: fmt.Sprintf("https://%s/confirmation/{checkout.order.id}", host),
|
Confirmation: fmt.Sprintf("https://%s/confirmation/{checkout.order.id}", r.Host),
|
||||||
Notification: "https://cart.tornberg.me/notification",
|
|
||||||
Validation: "https://cart.tornberg.me/validate",
|
|
||||||
Push: "https://cart.tornberg.me/push?order_id={checkout.order.id}",
|
|
||||||
Country: country,
|
Country: country,
|
||||||
Currency: getCurrency(country),
|
Currency: getCurrency(country),
|
||||||
Locale: getLocale(country),
|
Locale: getLocale(country),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current grain state (may be local or remote)
|
// Get current grain state (may be local or remote)
|
||||||
grain, err := s.Get(ctx, uint64(id))
|
grain, err := s.Get(r.Context(), uint64(id))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if s.inventoryService != nil {
|
if s.inventoryService != nil {
|
||||||
inventoryRequests := getInventoryRequests(grain.Items)
|
inventoryRequests := getInventoryRequests(grain.Items)
|
||||||
failingRequest, err := s.inventoryService.ReservationCheck(ctx, inventoryRequests...)
|
failingRequest, err := s.inventoryService.ReservationCheck(r.Context(), inventoryRequests...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.WarnContext(ctx, "inventory check failed", string(failingRequest.SKU), string(failingRequest.LocationID))
|
logger.WarnContext(r.Context(), "inventory check failed", string(failingRequest.SKU), string(failingRequest.LocationID))
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -370,9 +367,9 @@ func (s *PoolServer) CreateOrUpdateCheckout(ctx context.Context, host string, id
|
|||||||
}
|
}
|
||||||
|
|
||||||
if grain.OrderReference != "" {
|
if grain.OrderReference != "" {
|
||||||
return s.klarnaClient.UpdateOrder(ctx, grain.OrderReference, bytes.NewReader(payload))
|
return s.klarnaClient.UpdateOrder(r.Context(), grain.OrderReference, bytes.NewReader(payload))
|
||||||
} else {
|
} else {
|
||||||
return s.klarnaClient.CreateOrder(ctx, bytes.NewReader(payload))
|
return s.klarnaClient.CreateOrder(r.Context(), bytes.NewReader(payload))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +581,7 @@ func (s *PoolServer) CheckoutHandler(fn func(order *CheckoutOrder, w http.Respon
|
|||||||
return CookieCartIdHandler(s.ProxyHandler(func(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
return CookieCartIdHandler(s.ProxyHandler(func(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||||
orderId := r.URL.Query().Get("order_id")
|
orderId := r.URL.Query().Get("order_id")
|
||||||
if orderId == "" {
|
if orderId == "" {
|
||||||
order, err := s.CreateOrUpdateCheckout(r.Context(), r.Host, cartId)
|
order, err := s.CreateOrUpdateCheckout(r, cartId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("unable to create klarna session", "error", err)
|
logger.Error("unable to create klarna session", "error", err)
|
||||||
return err
|
return err
|
||||||
|
|||||||
@@ -133,3 +133,106 @@ func TestPriceMultiplyMethod(t *testing.T) {
|
|||||||
t.Fatalf("expected exVat %d got %d", exBefore*2, p.ValueExVat())
|
t.Fatalf("expected exVat %d got %d", exBefore*2, p.ValueExVat())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestGetTaxAmount(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
total int64
|
||||||
|
tax int
|
||||||
|
expected int64
|
||||||
|
desc string
|
||||||
|
}{
|
||||||
|
{1250, 2500, 250, "25% VAT"}, // 1250 / (1 + 100/25) = 1250 / 5 = 250
|
||||||
|
{1000, 2000, 166, "20% VAT"}, // 1000 / (1 + 100/20) = 1000 / 6 ≈ 166
|
||||||
|
{1200, 2500, 240, "25% VAT on 1200"},
|
||||||
|
{0, 2500, 0, "zero total"},
|
||||||
|
{100, 1000, 9, "10% VAT"}, // tax=1000 for 10%, 100 / (1 + 100/10) = 100 / 11 ≈ 9
|
||||||
|
{100, 10000, 50, "100% VAT"}, // tax=10000 for 100%, 100 / (1 + 100/100) = 100 / 2 = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
result := GetTaxAmount(tt.total, tt.tax)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("GetTaxAmount(%d, %d) [%s] = %d; expected %d", tt.total, tt.tax, tt.desc, result, tt.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewPriceFromIncVatEdgeCases(t *testing.T) {
|
||||||
|
// Zero VAT rate
|
||||||
|
p := NewPriceFromIncVat(1000, 0)
|
||||||
|
if p.IncVat != 1000 {
|
||||||
|
t.Errorf("expected IncVat 1000, got %d", p.IncVat)
|
||||||
|
}
|
||||||
|
if len(p.VatRates) != 1 || p.VatRates[0] != 0 {
|
||||||
|
t.Errorf("expected VAT 0 for rate 0, got %v", p.VatRates)
|
||||||
|
}
|
||||||
|
if p.ValueExVat() != 1000 {
|
||||||
|
t.Errorf("expected exVat 1000, got %d", p.ValueExVat())
|
||||||
|
}
|
||||||
|
|
||||||
|
// High VAT rate, e.g., 50%
|
||||||
|
p = NewPriceFromIncVat(1500, 50)
|
||||||
|
expectedVat := int64(1500 / (1 + 100/50)) // 1500 / 3 = 500
|
||||||
|
if p.VatRates[50] != expectedVat {
|
||||||
|
t.Errorf("expected VAT %d for 50%%, got %d", expectedVat, p.VatRates[50])
|
||||||
|
}
|
||||||
|
if p.ValueExVat() != 1500-expectedVat {
|
||||||
|
t.Errorf("expected exVat %d, got %d", 1500-expectedVat, p.ValueExVat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPriceValueExVatAndTotalVat(t *testing.T) {
|
||||||
|
p := Price{IncVat: 13700, VatRates: map[float32]int64{25: 2500, 12: 1200}}
|
||||||
|
exVat := p.ValueExVat()
|
||||||
|
totalVat := p.TotalVat()
|
||||||
|
if exVat != 10000 {
|
||||||
|
t.Errorf("expected exVat 10000, got %d", exVat)
|
||||||
|
}
|
||||||
|
if totalVat != 3700 {
|
||||||
|
t.Errorf("expected totalVat 3700, got %d", totalVat)
|
||||||
|
}
|
||||||
|
if exVat+totalVat != p.IncVat {
|
||||||
|
t.Errorf("exVat + totalVat should equal IncVat: %d + %d != %d", exVat, totalVat, p.IncVat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty VAT rates
|
||||||
|
p2 := Price{IncVat: 500, VatRates: nil}
|
||||||
|
if p2.ValueExVat() != 500 {
|
||||||
|
t.Errorf("expected exVat 500 for no VAT, got %d", p2.ValueExVat())
|
||||||
|
}
|
||||||
|
if p2.TotalVat() != 0 {
|
||||||
|
t.Errorf("expected totalVat 0, got %d", p2.TotalVat())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMultiplyPriceWithZeroQty(t *testing.T) {
|
||||||
|
base := Price{IncVat: 1250, VatRates: map[float32]int64{25: 250}}
|
||||||
|
multiplied := MultiplyPrice(base, 0)
|
||||||
|
if multiplied.IncVat != 0 {
|
||||||
|
t.Errorf("expected IncVat 0, got %d", multiplied.IncVat)
|
||||||
|
}
|
||||||
|
if len(multiplied.VatRates) != 1 || multiplied.VatRates[25] != 0 {
|
||||||
|
t.Errorf("expected VAT 0, got %v", multiplied.VatRates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPriceAddSubtractEdgeCases(t *testing.T) {
|
||||||
|
a := Price{IncVat: 1000, VatRates: map[float32]int64{25: 200}}
|
||||||
|
b := Price{IncVat: 500, VatRates: map[float32]int64{12: 54}} // Different rate
|
||||||
|
|
||||||
|
acc := NewPrice()
|
||||||
|
acc.Add(a)
|
||||||
|
acc.Add(b)
|
||||||
|
|
||||||
|
if acc.VatRates[25] != 200 || acc.VatRates[12] != 54 {
|
||||||
|
t.Errorf("expected VAT 25:200, 12:54, got %v", acc.VatRates)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subtract more than added (negative VAT)
|
||||||
|
acc.Subtract(a)
|
||||||
|
acc.Subtract(b)
|
||||||
|
acc.Subtract(a) // Subtract extra a
|
||||||
|
if acc.VatRates[25] != -200 || acc.VatRates[12] != 0 {
|
||||||
|
t.Errorf("expected negative VAT for 25 after over-subtract, got %v", acc.VatRates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ package discovery
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
|
"sync"
|
||||||
|
|
||||||
v1 "k8s.io/api/core/v1"
|
v1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@@ -33,13 +35,10 @@ func (k *K8sDiscovery) DiscoverInNamespace(namespace string) ([]string, error) {
|
|||||||
return hosts, nil
|
return hosts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type HostChange struct {
|
|
||||||
Host string
|
|
||||||
Type watch.EventType
|
|
||||||
}
|
|
||||||
|
|
||||||
func (k *K8sDiscovery) Watch() (<-chan HostChange, error) {
|
func (k *K8sDiscovery) Watch() (<-chan HostChange, error) {
|
||||||
timeout := int64(30)
|
timeout := int64(30)
|
||||||
|
ipsThatAreReady := make(map[string]bool)
|
||||||
|
m := sync.Mutex{}
|
||||||
watcherFn := func(options metav1.ListOptions) (watch.Interface, error) {
|
watcherFn := func(options metav1.ListOptions) (watch.Interface, error) {
|
||||||
return k.client.CoreV1().Pods("").Watch(k.ctx, metav1.ListOptions{
|
return k.client.CoreV1().Pods("").Watch(k.ctx, metav1.ListOptions{
|
||||||
LabelSelector: "actor-pool=cart",
|
LabelSelector: "actor-pool=cart",
|
||||||
@@ -55,10 +54,22 @@ func (k *K8sDiscovery) Watch() (<-chan HostChange, error) {
|
|||||||
for event := range watcher.ResultChan() {
|
for event := range watcher.ResultChan() {
|
||||||
|
|
||||||
pod := event.Object.(*v1.Pod)
|
pod := event.Object.(*v1.Pod)
|
||||||
// log.Printf("pod change %+v", pod.Status.Phase == v1.PodRunning)
|
isReady := slices.ContainsFunc(pod.Status.Conditions, func(condition v1.PodCondition) bool {
|
||||||
|
return condition.Type == v1.PodReady && condition.Status == v1.ConditionTrue
|
||||||
|
})
|
||||||
|
m.Lock()
|
||||||
|
oldState := ipsThatAreReady[pod.Status.PodIP]
|
||||||
|
ipsThatAreReady[pod.Status.PodIP] = isReady
|
||||||
|
m.Unlock()
|
||||||
|
if oldState != isReady {
|
||||||
ch <- HostChange{
|
ch <- HostChange{
|
||||||
Host: pod.Status.PodIP,
|
Host: pod.Status.PodIP,
|
||||||
Type: event.Type,
|
IsReady: isReady,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ch <- HostChange{
|
||||||
|
Host: pod.Status.PodIP,
|
||||||
|
IsReady: isReady,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|||||||
@@ -2,9 +2,8 @@ package discovery
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"slices"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/watch"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MockDiscovery is an in-memory Discovery implementation for tests.
|
// MockDiscovery is an in-memory Discovery implementation for tests.
|
||||||
@@ -56,14 +55,12 @@ func (m *MockDiscovery) AddHost(host string) {
|
|||||||
if m.closed {
|
if m.closed {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
for _, h := range m.hosts {
|
if slices.Contains(m.hosts, host) {
|
||||||
if h == host {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
m.hosts = append(m.hosts, host)
|
m.hosts = append(m.hosts, host)
|
||||||
if m.started {
|
if m.started {
|
||||||
m.events <- HostChange{Host: host, Type: watch.Added}
|
m.events <- HostChange{Host: host, IsReady: true}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,7 +83,7 @@ func (m *MockDiscovery) RemoveHost(host string) {
|
|||||||
}
|
}
|
||||||
m.hosts = append(m.hosts[:idx], m.hosts[idx+1:]...)
|
m.hosts = append(m.hosts[:idx], m.hosts[idx+1:]...)
|
||||||
if m.started {
|
if m.started {
|
||||||
m.events <- HostChange{Host: host, Type: watch.Deleted}
|
m.events <- HostChange{Host: host, IsReady: false}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package discovery
|
package discovery
|
||||||
|
|
||||||
|
type HostChange struct {
|
||||||
|
Host string
|
||||||
|
IsReady bool
|
||||||
|
}
|
||||||
|
|
||||||
type Discovery interface {
|
type Discovery interface {
|
||||||
Discover() ([]string, error)
|
Discover() ([]string, error)
|
||||||
Watch() (<-chan HostChange, error)
|
Watch() (<-chan HostChange, error)
|
||||||
|
|||||||
Reference in New Issue
Block a user