352 lines
9.8 KiB
Go
352 lines
9.8 KiB
Go
package ucp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
|
|
"git.k6n.net/mats/go-cart-actor/pkg/profile"
|
|
messages "git.k6n.net/mats/go-cart-actor/proto/profile"
|
|
)
|
|
|
|
// CustomerServer is the UCP REST adapter over the profile grain pool.
|
|
type CustomerServer struct {
|
|
applier ProfileApplier
|
|
}
|
|
|
|
// NewCustomerServer builds a UCP REST adapter over a profile grain pool.
|
|
func NewCustomerServer(applier ProfileApplier) *CustomerServer {
|
|
return &CustomerServer{applier: applier}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// UCP Customer endpoints
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// handleGetCustomer handles GET /customers/{id}.
|
|
func (s *CustomerServer) handleGetCustomer(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseCustomerID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid customer id")
|
|
return
|
|
}
|
|
|
|
g, err := s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "customer not found")
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, customerGrainToResponse(r.PathValue("id"), g))
|
|
}
|
|
|
|
// handleUpdateCustomer handles PUT /customers/{id}.
|
|
func (s *CustomerServer) handleUpdateCustomer(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseCustomerID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid customer id")
|
|
return
|
|
}
|
|
|
|
var req CustomerUpdateRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
|
return
|
|
}
|
|
|
|
msg := &messages.SetProfile{
|
|
Name: req.Name,
|
|
Email: req.Email,
|
|
Phone: req.Phone,
|
|
Language: req.Language,
|
|
Currency: req.Currency,
|
|
AvatarUrl: req.AvatarUrl,
|
|
}
|
|
|
|
if _, err := s.applier.Apply(r.Context(), id, msg); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to update customer: "+err.Error())
|
|
return
|
|
}
|
|
|
|
g, err := s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to read customer: "+err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, customerGrainToResponse(r.PathValue("id"), g))
|
|
}
|
|
|
|
// handleAddAddress handles POST /customers/{id}/addresses.
|
|
func (s *CustomerServer) handleAddAddress(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseCustomerID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid customer id")
|
|
return
|
|
}
|
|
|
|
var req AddAddressRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
|
return
|
|
}
|
|
|
|
if req.AddressLine1 == "" || req.City == "" || req.Zip == "" || req.Country == "" {
|
|
writeError(w, http.StatusBadRequest, "addressLine1, city, zip, and country are required")
|
|
return
|
|
}
|
|
|
|
addr := &messages.Address{
|
|
Label: req.Label,
|
|
FullName: req.FullName,
|
|
AddressLine1: req.AddressLine1,
|
|
City: req.City,
|
|
State: req.State,
|
|
Zip: req.Zip,
|
|
Country: req.Country,
|
|
IsDefaultShipping: req.IsDefaultShipping,
|
|
IsDefaultBilling: req.IsDefaultBilling,
|
|
}
|
|
if req.AddressLine2 != "" {
|
|
addr.AddressLine2 = &req.AddressLine2
|
|
}
|
|
if req.Phone != "" {
|
|
addr.Phone = &req.Phone
|
|
}
|
|
msg := &messages.AddAddress{Address: addr}
|
|
|
|
if _, err := s.applier.Apply(r.Context(), id, msg); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to add address: "+err.Error())
|
|
return
|
|
}
|
|
|
|
g, err := s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to read customer: "+err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusCreated, customerGrainToResponse(r.PathValue("id"), g))
|
|
}
|
|
|
|
// handleUpdateAddress handles PUT /customers/{id}/addresses/{addressId}.
|
|
func (s *CustomerServer) handleUpdateAddress(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseCustomerID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid customer id")
|
|
return
|
|
}
|
|
|
|
addressID, ok := parseAddressID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid address id")
|
|
return
|
|
}
|
|
|
|
var req UpdateAddressRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "invalid request body: "+err.Error())
|
|
return
|
|
}
|
|
|
|
msg := &messages.UpdateAddress{
|
|
Id: addressID,
|
|
Label: req.Label,
|
|
FullName: req.FullName,
|
|
AddressLine1: req.AddressLine1,
|
|
AddressLine2: req.AddressLine2,
|
|
City: req.City,
|
|
State: req.State,
|
|
Zip: req.Zip,
|
|
Country: req.Country,
|
|
Phone: req.Phone,
|
|
IsDefaultShipping: req.IsDefaultShipping,
|
|
IsDefaultBilling: req.IsDefaultBilling,
|
|
}
|
|
|
|
if _, err := s.applier.Apply(r.Context(), id, msg); err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to update address: "+err.Error())
|
|
return
|
|
}
|
|
|
|
g, err := s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to read customer: "+err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, customerGrainToResponse(r.PathValue("id"), g))
|
|
}
|
|
|
|
// handleRemoveAddress handles DELETE /customers/{id}/addresses/{addressId}.
|
|
func (s *CustomerServer) handleRemoveAddress(w http.ResponseWriter, r *http.Request) {
|
|
id, ok := parseCustomerID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid customer id")
|
|
return
|
|
}
|
|
|
|
addressID, ok := parseAddressID(r)
|
|
if !ok {
|
|
writeError(w, http.StatusBadRequest, "invalid address id")
|
|
return
|
|
}
|
|
|
|
msg := &messages.RemoveAddress{Id: addressID}
|
|
|
|
if _, err := s.applier.Apply(r.Context(), id, msg); err != nil {
|
|
// Check if the error is a "not found" error.
|
|
errStr := err.Error()
|
|
if containsNotFound(errStr) {
|
|
writeError(w, http.StatusNotFound, fmt.Sprintf("address %d not found", addressID))
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, "failed to remove address: "+errStr)
|
|
return
|
|
}
|
|
|
|
g, err := s.applier.Get(r.Context(), id)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, "failed to read customer: "+err.Error())
|
|
return
|
|
}
|
|
|
|
writeJSON(w, http.StatusOK, customerGrainToResponse(r.PathValue("id"), g))
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Identity linking helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// linkCartToProfile links a cart to the given profile by applying a LinkCart mutation.
|
|
func linkCartToProfile(applier ProfileApplier, ctx context.Context, profileID uint64, cartID uint64) error {
|
|
if applier == nil {
|
|
return nil
|
|
}
|
|
_, err := applier.Apply(ctx, profileID, &messages.LinkCart{
|
|
CartId: cartID,
|
|
Label: "current cart",
|
|
})
|
|
return err
|
|
}
|
|
|
|
// linkCheckoutToProfile links a checkout to the given profile by applying a LinkCheckout mutation.
|
|
func linkCheckoutToProfile(applier ProfileApplier, ctx context.Context, profileID uint64, checkoutID uint64, cartID uint64) error {
|
|
if applier == nil {
|
|
return nil
|
|
}
|
|
_, err := applier.Apply(ctx, profileID, &messages.LinkCheckout{
|
|
CheckoutId: checkoutID,
|
|
CartId: cartID,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// linkOrderToProfile links an order to the given profile by applying a LinkOrder mutation.
|
|
func linkOrderToProfile(applier ProfileApplier, ctx context.Context, profileID uint64, orderReference string, cartID uint64, status string) error {
|
|
if applier == nil {
|
|
return nil
|
|
}
|
|
_, err := applier.Apply(ctx, profileID, &messages.LinkOrder{
|
|
OrderReference: orderReference,
|
|
CartId: cartID,
|
|
Status: status,
|
|
})
|
|
return err
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// parseCustomerID parses the base62 customer id from the path.
|
|
func parseCustomerID(r *http.Request) (uint64, bool) {
|
|
raw := r.PathValue("id")
|
|
if raw == "" {
|
|
return 0, false
|
|
}
|
|
cid, ok := profile.ParseProfileId(raw)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return uint64(cid), true
|
|
}
|
|
|
|
// parseAddressID parses the numeric address id from the path.
|
|
func parseAddressID(r *http.Request) (uint32, bool) {
|
|
raw := r.PathValue("addressId")
|
|
if raw == "" {
|
|
return 0, false
|
|
}
|
|
// Address IDs are simple numeric strings.
|
|
var id uint32
|
|
for _, c := range raw {
|
|
if c < '0' || c > '9' {
|
|
return 0, false
|
|
}
|
|
id = id*10 + uint32(c-'0')
|
|
}
|
|
return id, true
|
|
}
|
|
|
|
// containsNotFound reports whether errStr indicates a "not found" condition.
|
|
func containsNotFound(errStr string) bool {
|
|
// Check for common not-found patterns in the error message.
|
|
for _, pattern := range []string{"not found", "not_found", "notfound"} {
|
|
if len(errStr) >= len(pattern) {
|
|
for i := 0; i <= len(errStr)-len(pattern); i++ {
|
|
match := true
|
|
for j := 0; j < len(pattern); j++ {
|
|
c1 := errStr[i+j]
|
|
c2 := pattern[j]
|
|
if c1 >= 'A' && c1 <= 'Z' {
|
|
c1 += 32
|
|
}
|
|
if c1 != c2 {
|
|
match = false
|
|
break
|
|
}
|
|
}
|
|
if match {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// customerGrainToResponse converts a ProfileGrain to a CustomerResponse.
|
|
func customerGrainToResponse(id string, g *profile.ProfileGrain) CustomerResponse {
|
|
resp := CustomerResponse{
|
|
Id: id,
|
|
Name: g.Name,
|
|
Email: g.Email,
|
|
Phone: g.Phone,
|
|
Language: g.Language,
|
|
Currency: g.Currency,
|
|
AvatarUrl: g.AvatarUrl,
|
|
Addresses: make([]AddressResponse, 0, len(g.Addresses)),
|
|
}
|
|
|
|
for _, a := range g.Addresses {
|
|
resp.Addresses = append(resp.Addresses, AddressResponse{
|
|
Id: a.Id,
|
|
Label: a.Label,
|
|
FullName: a.FullName,
|
|
AddressLine1: a.AddressLine1,
|
|
AddressLine2: a.AddressLine2,
|
|
City: a.City,
|
|
State: a.State,
|
|
Zip: a.Zip,
|
|
Country: a.Country,
|
|
Phone: a.Phone,
|
|
IsDefaultShipping: a.IsDefaultShipping,
|
|
IsDefaultBilling: a.IsDefaultBilling,
|
|
})
|
|
}
|
|
|
|
return resp
|
|
}
|