add profile actors
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user