diff --git a/cmd/backoffice/fileserver.go b/cmd/backoffice/fileserver.go index 0f6a86c..49cc32b 100644 --- a/cmd/backoffice/fileserver.go +++ b/cmd/backoffice/fileserver.go @@ -18,19 +18,24 @@ import ( "git.k6n.net/go-cart-actor/pkg/actor" "git.k6n.net/go-cart-actor/pkg/cart" + "git.k6n.net/go-cart-actor/pkg/checkout" "google.golang.org/protobuf/proto" ) type FileServer struct { // Define fields here - dataDir string - storage actor.LogStorage[cart.CartGrain] + dataDir string + checkoutDataDir string + storage actor.LogStorage[cart.CartGrain] + checkoutStorage actor.LogStorage[checkout.CheckoutGrain] } -func NewFileServer(dataDir string, storage actor.LogStorage[cart.CartGrain]) *FileServer { +func NewFileServer(dataDir string, checkoutDataDir string, storage actor.LogStorage[cart.CartGrain], checkoutStorage actor.LogStorage[checkout.CheckoutGrain]) *FileServer { return &FileServer{ - dataDir: dataDir, - storage: storage, + dataDir: dataDir, + checkoutDataDir: checkoutDataDir, + storage: storage, + checkoutStorage: checkoutStorage, } } @@ -79,6 +84,12 @@ func appendFileInfo(info fs.FileInfo, out *CartFileInfo) *CartFileInfo { return out } +func appendCheckoutFileInfo(info fs.FileInfo, out *CheckoutFileInfo) *CheckoutFileInfo { + out.Size = info.Size() + out.Modified = info.ModTime() + return out +} + // var cartFileRe = regexp.MustCompile(`^(\d+)\.events\.log$`) func listCartFiles(dir string) ([]*CartFileInfo, error) { @@ -112,6 +123,36 @@ func listCartFiles(dir string) ([]*CartFileInfo, error) { return out, nil } +func listCheckoutFiles(dir string) ([]*CheckoutFileInfo, error) { + entries, err := os.ReadDir(dir) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return []*CheckoutFileInfo{}, nil + } + return nil, err + } + out := make([]*CheckoutFileInfo, 0) + for _, e := range entries { + if e.IsDir() { + continue + } + id, valid := isValidFileId(e.Name()) + if !valid { + continue + } + + info, err := e.Info() + if err != nil { + continue + } + out = append(out, appendCheckoutFileInfo(info, &CheckoutFileInfo{ + ID: fmt.Sprintf("%d", id), + CheckoutId: checkout.CheckoutId(id), + })) + } + return out, nil +} + func readRawLogLines(path string) ([]json.RawMessage, error) { fh, err := os.Open(path) if err != nil { @@ -158,6 +199,21 @@ func (fs *FileServer) CartsHandler(w http.ResponseWriter, r *http.Request) { }) } +func (fs *FileServer) CheckoutsHandler(w http.ResponseWriter, r *http.Request) { + list, err := listCheckoutFiles(fs.checkoutDataDir) + if err != nil { + writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) + return + } + // sort by modified desc + sort.Slice(list, func(i, j int) bool { return list[i].Modified.After(list[j].Modified) }) + + writeJSON(w, http.StatusOK, map[string]any{ + "count": len(list), + "checkouts": list, + }) +} + func (fs *FileServer) PromotionsHandler(w http.ResponseWriter, r *http.Request) { fileName := filepath.Join(fs.dataDir, "promotions.json") if r.Method == http.MethodGet { @@ -315,3 +371,69 @@ func (fs *FileServer) CartHandler(w http.ResponseWriter, r *http.Request) { }, }) } + +func (fs *FileServer) CheckoutHandler(w http.ResponseWriter, r *http.Request) { + idStr := r.PathValue("id") + if idStr == "" { + writeJSON(w, http.StatusBadRequest, JsonError{Error: "missing id"}) + return + } + id, ok := isValidId(idStr) + if !ok { + writeJSON(w, http.StatusBadRequest, JsonError{Error: "invalid id"}) + return + } + // parse query parameters for filtering + query := r.URL.Query() + filterFunction := acceptAll + if maxIndexStr := query.Get("maxIndex"); maxIndexStr != "" { + log.Printf("filter maxIndex: %s", maxIndexStr) + maxIndex, err := strconv.Atoi(maxIndexStr) + if err != nil { + writeJSON(w, http.StatusBadRequest, JsonError{Error: "invalid maxIndex"}) + return + } + filterFunction = acceptUntilIndex(maxIndex) + } else if untilStr := query.Get("until"); untilStr != "" { + log.Printf("filter until: %s", untilStr) + until, err := time.Parse(time.RFC3339, untilStr) + if err != nil { + writeJSON(w, http.StatusBadRequest, JsonError{Error: "invalid until timestamp"}) + return + } + filterFunction = acceptUntilTimestamp(until) + } + // reconstruct state from event log if present + grain := checkout.NewCheckoutGrain(id, cart.CartId(id), 0, time.Now(), nil) + err := fs.checkoutStorage.LoadEventsFunc(r.Context(), id, grain, filterFunction) + if err != nil { + writeJSON(w, http.StatusInternalServerError, JsonError{Error: err.Error()}) + return + } + + path := filepath.Join(fs.checkoutDataDir, fmt.Sprintf("%d.events.log", id)) + info, err := os.Stat(path) + if err != nil && errors.Is(err, os.ErrNotExist) { + writeJSON(w, http.StatusNotFound, JsonError{Error: "checkout not found"}) + return + } else if err != nil { + writeJSON(w, http.StatusInternalServerError, JsonError{Error: err.Error()}) + return + } + lines, err := readRawLogLines(path) + if err != nil { + writeJSON(w, http.StatusInternalServerError, JsonError{Error: err.Error()}) + return + } + writeJSON(w, http.StatusOK, map[string]any{ + "id": id, + "checkoutId": checkout.CheckoutId(id).String(), + "state": grain, + "mutations": lines, + "meta": map[string]any{ + "size": info.Size(), + "modified": info.ModTime(), + "path": path, + }, + }) +} diff --git a/cmd/backoffice/main.go b/cmd/backoffice/main.go index 7fd958b..fd5b13d 100644 --- a/cmd/backoffice/main.go +++ b/cmd/backoffice/main.go @@ -11,6 +11,7 @@ import ( actor "git.k6n.net/go-cart-actor/pkg/actor" "git.k6n.net/go-cart-actor/pkg/cart" + "git.k6n.net/go-cart-actor/pkg/checkout" "github.com/matst80/go-redis-inventory/pkg/inventory" "github.com/matst80/slask-finder/pkg/messaging" amqp "github.com/rabbitmq/amqp091-go" @@ -24,6 +25,13 @@ type CartFileInfo struct { Modified time.Time `json:"modified"` } +type CheckoutFileInfo struct { + ID string `json:"id"` + CheckoutId checkout.CheckoutId `json:"checkoutId"` + Size int64 `json:"size"` + Modified time.Time `json:"modified"` +} + func envOrDefault(key, def string) string { if v := os.Getenv(key); v != "" { return v @@ -97,7 +105,13 @@ func main() { reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil)) diskStorage := actor.NewDiskStorage[cart.CartGrain](dataDir, reg) - fs := NewFileServer(dataDir, diskStorage) + checkoutDataDir := envOrDefault("CHECKOUT_DATA_DIR", "checkout-data") + _ = os.MkdirAll(checkoutDataDir, 0755) + + regCheckout := checkout.NewCheckoutMutationRegistry(checkout.NewCheckoutMutationContext()) + diskStorageCheckout := actor.NewDiskStorage[checkout.CheckoutGrain](checkoutDataDir, regCheckout) + + fs := NewFileServer(dataDir, checkoutDataDir, diskStorage, diskStorageCheckout) hub := NewHub() go hub.Run() @@ -105,6 +119,8 @@ func main() { mux := http.NewServeMux() mux.HandleFunc("GET /carts", fs.CartsHandler) mux.HandleFunc("GET /cart/{id}", fs.CartHandler) + mux.HandleFunc("GET /checkouts", fs.CheckoutsHandler) + mux.HandleFunc("GET /checkout/{id}", fs.CheckoutHandler) mux.HandleFunc("PUT /inventory/{locationId}/{sku}", func(w http.ResponseWriter, r *http.Request) { inventoryLocationId := inventory.LocationID(r.PathValue("locationId")) inventorySku := inventory.SKU(r.PathValue("sku")) diff --git a/cmd/checkout/adyen-handlers.go b/cmd/checkout/adyen-handlers.go index 9dd44a6..d8df68f 100644 --- a/cmd/checkout/adyen-handlers.go +++ b/cmd/checkout/adyen-handlers.go @@ -144,9 +144,10 @@ func (s *CheckoutPoolServer) AdyenHookHandler(w http.ResponseWriter, r *http.Req } if isSuccess { msgs = append(msgs, &messages.PaymentCompleted{ - PaymentId: item.PspReference, - Status: item.Success, - Amount: item.Amount.Value, + PaymentId: item.PspReference, + Status: item.Success, + Amount: item.Amount.Value, + CompletedAt: timestamppb.Now(), }) } else { msgs = append(msgs, &messages.PaymentDeclined{ diff --git a/cmd/checkout/klarna-handlers.go b/cmd/checkout/klarna-handlers.go index a99aa86..87e7f06 100644 --- a/cmd/checkout/klarna-handlers.go +++ b/cmd/checkout/klarna-handlers.go @@ -12,6 +12,7 @@ import ( "git.k6n.net/go-cart-actor/pkg/checkout" messages "git.k6n.net/go-cart-actor/proto/checkout" "github.com/matst80/go-redis-inventory/pkg/inventory" + "google.golang.org/protobuf/types/known/timestamppb" ) /* @@ -201,6 +202,7 @@ func (s *CheckoutPoolServer) KlarnaPushHandler(w http.ResponseWriter, r *http.Re ProcessorReference: &order.ID, Amount: int64(order.OrderAmount), Currency: order.PurchaseCurrency, + CompletedAt: timestamppb.Now(), }) // err = confirmOrder(r.Context(), order, orderHandler) diff --git a/deployment/deployment.yaml b/deployment/deployment.yaml index 49aba60..bbc66f9 100644 --- a/deployment/deployment.yaml +++ b/deployment/deployment.yaml @@ -38,7 +38,7 @@ spec: volumes: - name: data nfs: - path: /i-data/7a8af061/nfs/cart-actor + path: /i-data/7a8af061/nfs/ server: 10.10.1.10 serviceAccountName: default containers: @@ -76,6 +76,10 @@ spec: memory: "70Mi" cpu: "1200m" env: + - name: DATA_DIR + value: "/data/cart-actor" + - name: CHECKOUT_DATA_DIR + value: "/data/checkout-actor" - name: TZ value: "Europe/Stockholm" - name: REDIS_ADDRESS @@ -180,6 +184,10 @@ spec: memory: "70Mi" cpu: "1200m" env: + - name: DATA_DIR + value: "/data/cart-actor" + - name: CHECKOUT_DATA_DIR + value: "/data/checkout-actor" - name: TZ value: "Europe/Stockholm" - name: KLARNA_API_USERNAME diff --git a/pkg/checkout/mutation_payment_completed.go b/pkg/checkout/mutation_payment_completed.go index 34b16c6..619f5fc 100644 --- a/pkg/checkout/mutation_payment_completed.go +++ b/pkg/checkout/mutation_payment_completed.go @@ -21,9 +21,13 @@ func HandlePaymentCompleted(g *CheckoutGrain, m *messages.PaymentCompleted) erro payment.ProcessorReference = m.ProcessorReference payment.Status = PaymentStatusSuccess - payment.Amount = m.Amount - payment.Currency = m.Currency - payment.CompletedAt = &time.Time{} + if m.Amount > 0 { + payment.Amount = m.Amount + } + if m.Currency != "" { + payment.Currency = m.Currency + } + if m.CompletedAt != nil { *payment.CompletedAt = m.CompletedAt.AsTime() } else {