more cart
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
cartmcp "git.k6n.net/mats/go-cart-actor/pkg/cart/mcp"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart/recovery"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/promotions"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/proxy"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/telemetry"
|
||||
@@ -389,6 +390,65 @@ func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
// Data Retention (C5) GDPR Purge
|
||||
retentionDays := config.EnvInt("CART_RETENTION_DAYS", 0, func(n int) bool { return n >= 0 })
|
||||
if retentionDays > 0 {
|
||||
purgeInterval := config.EnvDuration("CART_PURGE_INTERVAL", 24*time.Hour)
|
||||
log.Printf("cart: data retention enabled. Retaining carts for %d days, purging every %v", retentionDays, purgeInterval)
|
||||
go func() {
|
||||
ticker := time.NewTicker(purgeInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Run immediately on startup
|
||||
runPurge(ctx, diskStorage, pool, retentionDays)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
runPurge(ctx, diskStorage, pool, retentionDays)
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
log.Print("cart: data retention / GDPR purge disabled (CART_RETENTION_DAYS not set or <= 0)")
|
||||
}
|
||||
|
||||
// Abandoned-cart recovery check (C3-notifications seam, dry-run by default).
|
||||
// Scans local pod event-log files for carts whose modtime falls into the
|
||||
// "[now - delay - window, now - delay]" window and that have items + a
|
||||
// contact (email and/or push tokens). Hands each candidate to the
|
||||
// configured Notifier (LoggingNotifier v0). The scanner reads local shard
|
||||
// files only — the control plane doesn't fan out between pods on purpose
|
||||
// (each pod owns the carts it is the owner of). Idempotency is the next
|
||||
// pass; the v0 window size keeps duplicate notifications minute-scoped.
|
||||
abandonedDelay := config.EnvDuration("CART_ABANDONED_DELAY", time.Hour)
|
||||
if abandonedDelay > 0 {
|
||||
abandonedWindow := config.EnvDuration("CART_ABANDONED_WINDOW", time.Hour)
|
||||
abandonedInterval := config.EnvDuration("CART_ABANDONED_SCAN_INTERVAL", 15*time.Minute)
|
||||
notifier := recovery.NewLoggingNotifier()
|
||||
scanner := recovery.NewScanner(config.EnvString("CART_DIR", "data"), diskStorage)
|
||||
log.Printf("cart: abandonment recovery ENABLED. Delay=%v window=%v scan_interval=%v (notifier=LoggingNotifier dry-run)", abandonedDelay, abandonedWindow, abandonedInterval)
|
||||
go func() {
|
||||
ticker := time.NewTicker(abandonedInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
runRecovery(ctx, scanner, notifier, abandonedDelay, abandonedWindow)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
runRecovery(ctx, scanner, notifier, abandonedDelay, abandonedWindow)
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
log.Print("cart: abandonment recovery DISABLED (CART_ABANDONED_DELAY not set or <= 0)")
|
||||
}
|
||||
|
||||
otelShutdown, err := telemetry.SetupOTelSDK(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to start otel %v", err)
|
||||
@@ -535,3 +595,47 @@ func main() {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func runPurge(ctx context.Context, diskStorage *actor.DiskStorage[cart.CartGrain], pool *actor.SimpleGrainPool[cart.CartGrain], retentionDays int) {
|
||||
log.Printf("cart: starting data retention purge...")
|
||||
retention := time.Duration(retentionDays) * 24 * time.Hour
|
||||
|
||||
localIds := make(map[uint64]bool)
|
||||
for _, id := range pool.GetLocalIds() {
|
||||
localIds[id] = true
|
||||
}
|
||||
isGrainActive := func(id uint64) bool {
|
||||
return localIds[id]
|
||||
}
|
||||
|
||||
purged, err := diskStorage.PurgeOlderThan(ctx, retention, isGrainActive)
|
||||
if err != nil {
|
||||
log.Printf("cart: data retention purge failed: %v", err)
|
||||
} else {
|
||||
log.Printf("cart: data retention purge completed. Purged %d expired cart logs", purged)
|
||||
}
|
||||
}
|
||||
|
||||
// runRecovery is the abandoned-cart scanner pass. It finds carts that crossed
|
||||
// the abandonment threshold in the recent window and hands each to the
|
||||
// configured Notifier. Each pass is independent; idempotency across ticks is
|
||||
// the next pass (logged duplicates are not catastrophic for the dry-run
|
||||
// notifier, but a real MailerSend/FCM impl will need a SETNX side-key).
|
||||
func runRecovery(ctx context.Context, scanner *recovery.Scanner, notifier recovery.Notifier, delay, window time.Duration) {
|
||||
candidates, err := scanner.Scan(ctx, delay, window, time.Now())
|
||||
if err != nil {
|
||||
log.Printf("cart: recovery scan failed: %v", err)
|
||||
return
|
||||
}
|
||||
if len(candidates) == 0 {
|
||||
log.Printf("cart: recovery scan complete, 0 candidates (window delay=%v width=%v)", delay, window)
|
||||
return
|
||||
}
|
||||
log.Printf("cart: recovery scan complete, %d candidates", len(candidates))
|
||||
for _, c := range candidates {
|
||||
if err := notifier.NotifyAbandoned(ctx, c); err != nil {
|
||||
log.Printf("cart: recovery notify (cart %d) failed: %v", c.CartID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -605,6 +605,49 @@ func (s *PoolServer) RemoveVoucherHandler(w http.ResponseWriter, r *http.Request
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetRecoveryContactRequest is the wire shape clients send to
|
||||
// PUT /cart/recovery-contact. It mirrors proto SetRecoveryContact with
|
||||
// JSON-friendly types; sending an empty JSON object clears all contact info.
|
||||
type SetRecoveryContactRequest struct {
|
||||
Email string `json:"email,omitempty"`
|
||||
PushTokens []cart.PushToken `json:"pushTokens,omitempty"`
|
||||
}
|
||||
|
||||
// toMessage builds the proto message from the JSON request body.
|
||||
func (s *SetRecoveryContactRequest) toMessage() *messages.SetRecoveryContact {
|
||||
out := &messages.SetRecoveryContact{Email: s.Email}
|
||||
if len(s.PushTokens) > 0 {
|
||||
out.PushTokens = make([]*messages.PushToken, 0, len(s.PushTokens))
|
||||
for _, t := range s.PushTokens {
|
||||
out.PushTokens = append(out.PushTokens, &messages.PushToken{
|
||||
Platform: t.Platform,
|
||||
Token: t.Token,
|
||||
})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *PoolServer) SetRecoveryContactHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||
if r.Method != http.MethodPut {
|
||||
w.WriteHeader(http.StatusMethodNotAllowed)
|
||||
return nil
|
||||
}
|
||||
req := SetRecoveryContactRequest{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
w.Write([]byte(err.Error()))
|
||||
return err
|
||||
}
|
||||
reply, err := s.ApplyLocal(r.Context(), cartId, req.toMessage())
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte(err.Error()))
|
||||
return err
|
||||
}
|
||||
return s.WriteResult(w, reply)
|
||||
}
|
||||
|
||||
func (s *PoolServer) SetUserIdHandler(w http.ResponseWriter, r *http.Request, cartId cart.CartId) error {
|
||||
setUserId := messages.SetUserId{}
|
||||
err := json.NewDecoder(r.Body).Decode(&setUserId)
|
||||
@@ -747,6 +790,10 @@ func (s *PoolServer) Serve(mux *http.ServeMux) {
|
||||
handleFunc("PUT /cart/subscription-details", CookieCartIdHandler(s.ProxyHandler(s.SubscriptionDetailsHandler)))
|
||||
handleFunc("DELETE /cart/voucher/{voucherId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveVoucherHandler)))
|
||||
handleFunc("PUT /cart/user", CookieCartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
|
||||
// PUT only — SetRecoveryContact is PUT/replace semantics. We deliberately
|
||||
// do NOT allow POST here to avoid conflicts with any future POST→create
|
||||
// semantics on /cart paths.
|
||||
handleFunc("PUT /cart/recovery-contact", CookieCartIdHandler(s.ProxyHandler(s.SetRecoveryContactHandler)))
|
||||
|
||||
handleFunc("PUT /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler)))
|
||||
handleFunc("DELETE /cart/item/{itemId}/marking", CookieCartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
|
||||
@@ -763,6 +810,7 @@ func (s *PoolServer) Serve(mux *http.ServeMux) {
|
||||
handleFunc("PUT /cart/byid/{id}/voucher", CookieCartIdHandler(s.ProxyHandler(s.AddVoucherHandler)))
|
||||
handleFunc("DELETE /cart/byid/{id}/voucher/{voucherId}", CookieCartIdHandler(s.ProxyHandler(s.RemoveVoucherHandler)))
|
||||
handleFunc("PUT /cart/byid/{id}/user", CartIdHandler(s.ProxyHandler(s.SetUserIdHandler)))
|
||||
handleFunc("PUT /cart/byid/{id}/recovery-contact", CartIdHandler(s.ProxyHandler(s.SetRecoveryContactHandler)))
|
||||
handleFunc("PUT /cart/byid/{id}/item/{itemId}/marking", CartIdHandler(s.ProxyHandler(s.LineItemMarkingHandler)))
|
||||
handleFunc("DELETE /cart/byid/{id}/item/{itemId}/marking", CartIdHandler(s.ProxyHandler(s.RemoveLineItemMarkingHandler)))
|
||||
handleFunc("PUT /cart/byid/{id}/item/{itemId}/custom-fields", CartIdHandler(s.ProxyHandler(s.SetCustomFieldsHandler)))
|
||||
|
||||
@@ -151,6 +151,33 @@ func BuildCheckoutOrderPayload(grain *checkout.CheckoutGrain, meta *CheckoutMeta
|
||||
})
|
||||
}
|
||||
|
||||
// Reflected applied promotions as negative discount lines
|
||||
if grain.CartState != nil {
|
||||
for _, ap := range grain.CartState.AppliedPromotions {
|
||||
if ap.Pending {
|
||||
continue
|
||||
}
|
||||
discountVal := int64(0)
|
||||
if ap.Discount != nil {
|
||||
discountVal = ap.Discount.IncVat.Int64()
|
||||
}
|
||||
if discountVal <= 0 {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, &Line{
|
||||
Type: "discount",
|
||||
Reference: "promo-" + ap.PromotionId,
|
||||
Name: "Promotion: " + ap.Name,
|
||||
Quantity: 1,
|
||||
UnitPrice: int(-discountVal),
|
||||
QuantityUnit: "st",
|
||||
TotalAmount: int(-discountVal),
|
||||
TaxRate: 2500,
|
||||
TotalTaxAmount: int(-discountVal * 2500 / 12500),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
order := &CheckoutOrder{
|
||||
PurchaseCountry: country,
|
||||
PurchaseCurrency: currency,
|
||||
@@ -285,6 +312,30 @@ func BuildAdyenCheckoutSession(grain *checkout.CheckoutGrain, meta *CheckoutMeta
|
||||
})
|
||||
}
|
||||
|
||||
// Reflected applied promotions as negative discount lines
|
||||
if grain.CartState != nil {
|
||||
for _, ap := range grain.CartState.AppliedPromotions {
|
||||
if ap.Pending {
|
||||
continue
|
||||
}
|
||||
discountVal := int64(0)
|
||||
if ap.Discount != nil {
|
||||
discountVal = ap.Discount.IncVat.Int64()
|
||||
}
|
||||
if discountVal <= 0 {
|
||||
continue
|
||||
}
|
||||
lineItems = append(lineItems, adyenCheckout.LineItem{
|
||||
Quantity: common.PtrInt64(1),
|
||||
AmountIncludingTax: common.PtrInt64(-discountVal),
|
||||
Description: common.PtrString("Promotion: " + ap.Name),
|
||||
AmountExcludingTax: common.PtrInt64(-discountVal * 10000 / 12500),
|
||||
TaxAmount: common.PtrInt64(-discountVal * 2500 / 12500),
|
||||
TaxPercentage: common.PtrInt64(2500),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return &adyenCheckout.CreateCheckoutSessionRequest{
|
||||
Reference: grain.Id.String(),
|
||||
Amount: adyenCheckout.Amount{
|
||||
|
||||
@@ -212,6 +212,31 @@ func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
// Data Retention (C5) GDPR Purge
|
||||
retentionDays := config.EnvInt("CHECKOUT_RETENTION_DAYS", 0, func(n int) bool { return n >= 0 })
|
||||
if retentionDays > 0 {
|
||||
purgeInterval := config.EnvDuration("CHECKOUT_PURGE_INTERVAL", 24*time.Hour)
|
||||
log.Printf("checkout: data retention enabled. Retaining checkouts for %d days, purging every %v", retentionDays, purgeInterval)
|
||||
go func() {
|
||||
ticker := time.NewTicker(purgeInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Run immediately on startup
|
||||
runPurge(ctx, diskStorage, pool, retentionDays)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
runPurge(ctx, diskStorage, pool, retentionDays)
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
log.Print("checkout: data retention / GDPR purge disabled (CHECKOUT_RETENTION_DAYS not set or <= 0)")
|
||||
}
|
||||
|
||||
otelShutdown, err := telemetry.SetupOTelSDK(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to start otel %v", err)
|
||||
@@ -299,3 +324,24 @@ func main() {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
func runPurge(ctx context.Context, diskStorage *actor.DiskStorage[checkout.CheckoutGrain], pool *actor.SimpleGrainPool[checkout.CheckoutGrain], retentionDays int) {
|
||||
log.Printf("checkout: starting data retention purge...")
|
||||
retention := time.Duration(retentionDays) * 24 * time.Hour
|
||||
|
||||
localIds := make(map[uint64]bool)
|
||||
for _, id := range pool.GetLocalIds() {
|
||||
localIds[id] = true
|
||||
}
|
||||
isGrainActive := func(id uint64) bool {
|
||||
return localIds[id]
|
||||
}
|
||||
|
||||
purged, err := diskStorage.PurgeOlderThan(ctx, retention, isGrainActive)
|
||||
if err != nil {
|
||||
log.Printf("checkout: data retention purge failed: %v", err)
|
||||
} else {
|
||||
log.Printf("checkout: data retention purge completed. Purged %d expired checkout logs", purged)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,6 +187,31 @@ func main() {
|
||||
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer stop()
|
||||
|
||||
// Data Retention (C5) GDPR Purge
|
||||
retentionDays := config.EnvInt("PROFILE_RETENTION_DAYS", 0, func(n int) bool { return n >= 0 })
|
||||
if retentionDays > 0 {
|
||||
purgeInterval := config.EnvDuration("PROFILE_PURGE_INTERVAL", 24*time.Hour)
|
||||
log.Printf("profile: data retention enabled. Retaining profiles for %d days, purging every %v", retentionDays, purgeInterval)
|
||||
go func() {
|
||||
ticker := time.NewTicker(purgeInterval)
|
||||
defer ticker.Stop()
|
||||
|
||||
// Run immediately on startup
|
||||
runPurge(ctx, diskStorage, pool, retentionDays)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
runPurge(ctx, diskStorage, pool, retentionDays)
|
||||
}
|
||||
}
|
||||
}()
|
||||
} else {
|
||||
log.Print("profile: data retention / GDPR purge disabled (PROFILE_RETENTION_DAYS not set or <= 0)")
|
||||
}
|
||||
|
||||
otelShutdown, err := telemetry.SetupOTelSDK(ctx)
|
||||
if err != nil {
|
||||
log.Fatalf("Unable to start otel %v", err)
|
||||
@@ -292,3 +317,24 @@ func main() {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
|
||||
func runPurge(ctx context.Context, diskStorage *actor.DiskStorage[profile.ProfileGrain], pool *actor.SimpleGrainPool[profile.ProfileGrain], retentionDays int) {
|
||||
log.Printf("profile: starting data retention purge...")
|
||||
retention := time.Duration(retentionDays) * 24 * time.Hour
|
||||
|
||||
localIds := make(map[uint64]bool)
|
||||
for _, id := range pool.GetLocalIds() {
|
||||
localIds[id] = true
|
||||
}
|
||||
isGrainActive := func(id uint64) bool {
|
||||
return localIds[id]
|
||||
}
|
||||
|
||||
purged, err := diskStorage.PurgeOlderThan(ctx, retention, isGrainActive)
|
||||
if err != nil {
|
||||
log.Printf("profile: data retention purge failed: %v", err)
|
||||
} else {
|
||||
log.Printf("profile: data retention purge completed. Purged %d expired profile logs", purged)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -230,6 +230,10 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: CART_RETENTION_DAYS
|
||||
value: "30"
|
||||
- name: CART_PURGE_INTERVAL
|
||||
value: "24h"
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -350,6 +354,10 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: CART_RETENTION_DAYS
|
||||
value: "30"
|
||||
- name: CART_PURGE_INTERVAL
|
||||
value: "24h"
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
@@ -484,6 +492,10 @@ spec:
|
||||
valueFrom:
|
||||
fieldRef:
|
||||
fieldPath: metadata.name
|
||||
- name: CHECKOUT_RETENTION_DAYS
|
||||
value: "30"
|
||||
- name: CHECKOUT_PURGE_INTERVAL
|
||||
value: "24h"
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
|
||||
@@ -189,6 +189,7 @@ func cartGrainToResponse(id string, g *cart.CartGrain) CartResponse {
|
||||
Items: make([]CartItem, 0, len(g.Items)),
|
||||
Totals: Totals{Currency: g.Currency},
|
||||
Status: "active",
|
||||
AppliedPromotions: g.AppliedPromotions,
|
||||
}
|
||||
if g.CheckoutStatus != nil && *g.CheckoutStatus != "" {
|
||||
resp.Status = string(*g.CheckoutStatus)
|
||||
|
||||
@@ -542,6 +542,16 @@ func (c *OrderHTTPClient) CreateOrder(ctx context.Context, checkoutID uint64, g
|
||||
// buildOrderLines converts a checkout grain's cart items and delivery selections
|
||||
// into order lines, matching the format expected by the order service.
|
||||
func buildOrderLines(g *checkout.CheckoutGrain) []orderLine {
|
||||
hasFreeShipping := false
|
||||
if g.CartState != nil {
|
||||
for _, ap := range g.CartState.AppliedPromotions {
|
||||
if ap.Type == "free_shipping" && !ap.Pending {
|
||||
hasFreeShipping = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
lines := make([]orderLine, 0, len(g.CartState.Items)+len(g.Deliveries))
|
||||
for _, it := range g.CartState.Items {
|
||||
if it == nil {
|
||||
@@ -569,7 +579,14 @@ func buildOrderLines(g *checkout.CheckoutGrain) []orderLine {
|
||||
})
|
||||
}
|
||||
for _, d := range g.Deliveries {
|
||||
if d == nil || d.Price.IncVat <= 0 {
|
||||
if d == nil {
|
||||
continue
|
||||
}
|
||||
unitPrice := d.Price.IncVat
|
||||
if hasFreeShipping {
|
||||
unitPrice = 0
|
||||
}
|
||||
if unitPrice <= 0 && !hasFreeShipping {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, orderLine{
|
||||
@@ -577,10 +594,35 @@ func buildOrderLines(g *checkout.CheckoutGrain) []orderLine {
|
||||
Sku: d.Provider,
|
||||
Name: "Delivery",
|
||||
Quantity: 1,
|
||||
UnitPrice: d.Price.IncVat,
|
||||
UnitPrice: unitPrice,
|
||||
TaxRate: 2500, // 25% in basis points
|
||||
})
|
||||
}
|
||||
|
||||
// Reflected applied promotions as negative discount lines
|
||||
if g.CartState != nil {
|
||||
for _, ap := range g.CartState.AppliedPromotions {
|
||||
if ap.Pending {
|
||||
continue
|
||||
}
|
||||
discountVal := int64(0)
|
||||
if ap.Discount != nil {
|
||||
discountVal = ap.Discount.IncVat.Int64()
|
||||
}
|
||||
if discountVal <= 0 {
|
||||
continue
|
||||
}
|
||||
lines = append(lines, orderLine{
|
||||
Reference: "promo-" + ap.PromotionId,
|
||||
Sku: "promo-" + ap.PromotionId,
|
||||
Name: "Promotion: " + ap.Name,
|
||||
Quantity: 1,
|
||||
UnitPrice: money.Cents(-discountVal),
|
||||
TaxRate: 2500, // default standard tax rate (25%) in basis points
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
|
||||
@@ -126,6 +126,7 @@ type CartResponse struct {
|
||||
Items []CartItem `json:"items,omitempty"`
|
||||
Totals Totals `json:"totals"`
|
||||
Status string `json:"status"`
|
||||
AppliedPromotions []cart.AppliedPromotion `json:"appliedPromotions,omitempty"`
|
||||
}
|
||||
|
||||
// CartItem is the UCP wire format for a cart line item in responses.
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -201,6 +202,9 @@ func (s *DiskStorage[V]) SaveLoop(duration time.Duration) {
|
||||
}
|
||||
|
||||
func (s *DiskStorage[V]) save() {
|
||||
if s.queue == nil {
|
||||
return
|
||||
}
|
||||
carts := 0
|
||||
lines := 0
|
||||
s.queue.Range(func(key, value any) bool {
|
||||
@@ -295,3 +299,65 @@ func (s *DiskStorage[V]) openWriterCount() int {
|
||||
defer s.writersMu.Unlock()
|
||||
return len(s.writers)
|
||||
}
|
||||
|
||||
func (s *DiskStorage[V]) PurgeOlderThan(ctx context.Context, retention time.Duration, isGrainActive func(id uint64) bool) (int, error) {
|
||||
if err := s.ensureDir(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
files, err := os.ReadDir(s.path)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("read storage dir: %w", err)
|
||||
}
|
||||
|
||||
cutoff := time.Now().Add(-retention)
|
||||
purgedCount := 0
|
||||
|
||||
for _, entry := range files {
|
||||
if entry.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := entry.Name()
|
||||
if !strings.HasSuffix(name, ".events.log") {
|
||||
continue
|
||||
}
|
||||
|
||||
var id uint64
|
||||
_, err := fmt.Sscanf(name, "%d.events.log", &id)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
log.Printf("failed to get file info for %s: %v", name, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if info.ModTime().Before(cutoff) {
|
||||
// Check if grain is active in memory
|
||||
if isGrainActive != nil && isGrainActive(id) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if we have an active writer open
|
||||
s.writersMu.Lock()
|
||||
_, hasWriter := s.writers[id]
|
||||
s.writersMu.Unlock()
|
||||
if hasWriter {
|
||||
continue
|
||||
}
|
||||
|
||||
// Delete the file
|
||||
fullPath := filepath.Join(s.path, name)
|
||||
if err := os.Remove(fullPath); err != nil {
|
||||
log.Printf("failed to delete expired cart log %s: %v", fullPath, err)
|
||||
} else {
|
||||
purgedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return purgedCount, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package actor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -317,3 +318,86 @@ func TestDiskStorageConcurrentWritesDifferentIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDiskStoragePurge(t *testing.T) {
|
||||
reg := diskTestRegistry(t)
|
||||
dir := t.TempDir()
|
||||
storage := NewDiskStorage[testState](dir, reg)
|
||||
t.Cleanup(storage.Close)
|
||||
|
||||
ctx := context.Background()
|
||||
msg := &cart_messages.AddItem{ItemId: 1, Quantity: 1}
|
||||
|
||||
// Write mutations to create event logs for 1, 2, 3, 4
|
||||
ids := []uint64{1, 2, 3, 4}
|
||||
for _, id := range ids {
|
||||
if err := storage.AppendMutations(id, msg); err != nil {
|
||||
t.Fatalf("append %d: %v", id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Force save to disk
|
||||
storage.save()
|
||||
|
||||
// 1. Grain 1: Old modification time, inactive, no writer -> should be purged
|
||||
path1 := storage.logPath(1)
|
||||
oldTime := time.Now().Add(-10 * 24 * time.Hour)
|
||||
if err := os.Chtimes(path1, oldTime, oldTime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We must close the writer for grain 1, 2, 4 to make them eligible for purging (no open writer)
|
||||
storage.writersMu.Lock()
|
||||
for _, id := range []uint64{1, 2, 4} {
|
||||
if w, ok := storage.writers[id]; ok {
|
||||
w.purged = true
|
||||
w.file.Close()
|
||||
delete(storage.writers, id)
|
||||
}
|
||||
}
|
||||
storage.writersMu.Unlock()
|
||||
|
||||
// 2. Grain 2: Old mod time, but marked active -> should NOT be purged
|
||||
path2 := storage.logPath(2)
|
||||
if err := os.Chtimes(path2, oldTime, oldTime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 3. Grain 3: Old mod time, but has open writer -> should NOT be purged
|
||||
path3 := storage.logPath(3)
|
||||
if err := os.Chtimes(path3, oldTime, oldTime); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// 4. Grain 4: Recent mod time -> should NOT be purged
|
||||
path4 := storage.logPath(4)
|
||||
|
||||
activeGrains := map[uint64]bool{2: true}
|
||||
isGrainActive := func(id uint64) bool {
|
||||
return activeGrains[id]
|
||||
}
|
||||
|
||||
purged, err := storage.PurgeOlderThan(ctx, 5*24*time.Hour, isGrainActive)
|
||||
if err != nil {
|
||||
t.Fatalf("PurgeOlderThan failed: %v", err)
|
||||
}
|
||||
|
||||
if purged != 1 {
|
||||
t.Errorf("Expected exactly 1 purged file, got %d", purged)
|
||||
}
|
||||
|
||||
// Verify file status
|
||||
if _, err := os.Stat(path1); !os.IsNotExist(err) {
|
||||
t.Error("Expected grain 1 log to be deleted")
|
||||
}
|
||||
if _, err := os.Stat(path2); err != nil {
|
||||
t.Error("Expected grain 2 log to exist (active)")
|
||||
}
|
||||
if _, err := os.Stat(path3); err != nil {
|
||||
t.Error("Expected grain 3 log to exist (active writer)")
|
||||
}
|
||||
if _, err := os.Stat(path4); err != nil {
|
||||
t.Error("Expected grain 4 log to exist (recent)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,12 +15,14 @@ import (
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
actor "git.k6n.net/mats/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/checkout"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/profile"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/promotions"
|
||||
"git.k6n.net/mats/platform/rabbit"
|
||||
amqp "github.com/rabbitmq/amqp091-go"
|
||||
)
|
||||
@@ -75,6 +77,69 @@ func New(cfg Config) (*App, error) {
|
||||
|
||||
_ = os.MkdirAll(cfg.DataDir, 0755)
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
|
||||
promotionsPath := os.Getenv("PROMOTIONS_FILE")
|
||||
if promotionsPath == "" {
|
||||
promotionsPath = "data/promotions.json"
|
||||
}
|
||||
if promotionStore, err := promotions.NewStore(promotionsPath); err == nil {
|
||||
promotionService := promotions.NewPromotionService(nil)
|
||||
reg.RegisterProcessor(
|
||||
actor.NewMutationProcessor(func(ctx context.Context, g *cart.CartGrain) error {
|
||||
for _, v := range g.Vouchers {
|
||||
if v != nil {
|
||||
v.BypassedByPromotions = false
|
||||
}
|
||||
}
|
||||
|
||||
g.UpdateTotals()
|
||||
promotionCtx := promotions.NewContextFromCart(g, promotions.WithNow(time.Now()))
|
||||
results, _ := promotionService.EvaluateAll(promotionStore.Snapshot(), promotionCtx)
|
||||
|
||||
hasBypassed := false
|
||||
for _, res := range results {
|
||||
if res.Applicable {
|
||||
promotions.WalkConditions(res.Rule.Conditions, func(c promotions.Condition) bool {
|
||||
if bc, ok := c.(promotions.BaseCondition); ok && bc.Type == promotions.CondCouponCode {
|
||||
var codes []string
|
||||
if s, ok := bc.Value.AsString(); ok {
|
||||
codes = append(codes, strings.ToLower(s))
|
||||
} else if arr, ok := bc.Value.AsStringSlice(); ok {
|
||||
for _, s := range arr {
|
||||
codes = append(codes, strings.ToLower(s))
|
||||
}
|
||||
}
|
||||
for _, code := range codes {
|
||||
for _, v := range g.Vouchers {
|
||||
if v != nil && strings.ToLower(v.Code) == code {
|
||||
if !v.BypassedByPromotions {
|
||||
v.BypassedByPromotions = true
|
||||
hasBypassed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if hasBypassed {
|
||||
g.UpdateTotals()
|
||||
promotionCtx = promotions.NewContextFromCart(g, promotions.WithNow(time.Now()))
|
||||
results, _ = promotionService.EvaluateAll(promotionStore.Snapshot(), promotionCtx)
|
||||
}
|
||||
|
||||
promotionService.ApplyResults(g, results, promotionCtx)
|
||||
g.Version++
|
||||
return nil
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
log.Printf("backofficeadmin: unable to load promotions store from %s: %v", promotionsPath, err)
|
||||
}
|
||||
|
||||
diskStorage := actor.NewDiskStorage[cart.CartGrain](cfg.DataDir, reg)
|
||||
|
||||
_ = os.MkdirAll(cfg.CheckoutDataDir, 0755)
|
||||
|
||||
@@ -86,6 +86,25 @@ type Notice struct {
|
||||
Code *string `json:"code,omitempty"`
|
||||
}
|
||||
|
||||
// PushToken is the Go-level mirror of cart_messages.PushToken: a generic,
|
||||
// provider-agnostic push-delivery target. Stored on the grain so the
|
||||
// abandoned-cart recovery scanner can hand it to a notifier without
|
||||
// re-decoding proto messages; the wire shape lives in proto/cart.proto.
|
||||
//
|
||||
// SECURITY: the raw Token persists verbatim to the per-pod event log
|
||||
// (CartGrain JSON-marshals PushTokens on every mutation). Anyone with read
|
||||
// access to the cart service's data dir — PVC snapshots, debug dumps, log
|
||||
// archival — can extract device handles. A future hardening pass should
|
||||
// either hash-on-write (hash.compareAtLaunch) for stateless matching or
|
||||
// encrypt-at-rest with a key the notifier owns. v0 keeps the seam: a real
|
||||
// notifier should treat the stored token as opaque, fetch any canonical
|
||||
// canonicalised handle from the input side (frontend/web SDK), and use
|
||||
// what's here only for routing.
|
||||
type PushToken struct {
|
||||
Platform string `json:"platform"`
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type CartPaymentStatus string
|
||||
|
||||
const (
|
||||
@@ -151,6 +170,14 @@ type CartGrain struct {
|
||||
Notifications []CartNotification `json:"cartNotification,omitempty"`
|
||||
SubscriptionDetails map[string]*SubscriptionDetails `json:"subscriptionDetails,omitempty"`
|
||||
|
||||
// Email is the cart's recovery contact address. Set explicitly via the
|
||||
// SetRecoveryContact mutation (independent of the linked profile's email),
|
||||
// so abandoned-cart recovery can fire before login. Empty string = no email.
|
||||
Email string `json:"email,omitempty"`
|
||||
// PushTokens holds every push delivery target the shopper attached to this
|
||||
// cart. Empty list = no push. PUT-style replaced by SetRecoveryContact.
|
||||
PushTokens []PushToken `json:"pushTokens,omitempty"`
|
||||
|
||||
//CheckoutOrderId string `json:"checkoutOrderId,omitempty"`
|
||||
CheckoutStatus *CartPaymentStatus `json:"checkoutStatus,omitempty"`
|
||||
//CheckoutCountry string `json:"checkoutCountry,omitempty"`
|
||||
|
||||
@@ -88,6 +88,7 @@ func NewCartMultationRegistry(context *CartMutationContext) actor.MutationRegist
|
||||
actor.NewMutation(SetLineItemCustomFields),
|
||||
actor.NewMutation(SubscriptionAdded),
|
||||
actor.NewMutation(context.SetCartType),
|
||||
actor.NewMutation(SetRecoveryContact),
|
||||
// actor.NewMutation(SubscriptionRemoved),
|
||||
)
|
||||
return reg
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
|
||||
messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
// SetRecoveryContact replaces the entire recovery-contact bundle on a cart in
|
||||
// a single event. PUT-style semantics — empty email and an empty token list
|
||||
// both persist as "no contact attached"; callers wanting to clear should send
|
||||
// an empty bundle.
|
||||
//
|
||||
// Trivial whitespace-only normalization is intentionally NOT done here: the
|
||||
// notifier layer owns validation (and the LoggingNotifier just logs). A future
|
||||
// real notifier does its own normalization before sending, so we don't want
|
||||
// to lose the original input.
|
||||
func SetRecoveryContact(grain *CartGrain, req *messages.SetRecoveryContact) error {
|
||||
if req == nil {
|
||||
return errors.New("SetRecoveryContact: nil payload")
|
||||
}
|
||||
grain.Email = req.Email
|
||||
if len(req.PushTokens) == 0 {
|
||||
grain.PushTokens = nil
|
||||
return nil
|
||||
}
|
||||
tokens := make([]PushToken, 0, len(req.PushTokens))
|
||||
dropped := 0
|
||||
for _, t := range req.PushTokens {
|
||||
if t == nil {
|
||||
// Defensive: a malformed wire request left a nil slot in the
|
||||
// repeated field. Skip rather than crash and surface at debug
|
||||
// level so a misbehaving client is visible without spamming.
|
||||
dropped++
|
||||
continue
|
||||
}
|
||||
tokens = append(tokens, PushToken{
|
||||
Platform: t.Platform,
|
||||
Token: t.Token,
|
||||
})
|
||||
}
|
||||
if dropped > 0 {
|
||||
log.Printf("SetRecoveryContact: dropped %d nil push-token entries (malformed wire)", dropped)
|
||||
}
|
||||
if len(tokens) == 0 {
|
||||
grain.PushTokens = nil
|
||||
return nil
|
||||
}
|
||||
grain.PushTokens = tokens
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package cart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
// TestSetRecoveryContact_ReplacesEntireBundle verifies PUT semantics: a
|
||||
// second call fully overwrites the first; calling with empty clears it.
|
||||
func TestSetRecoveryContact_ReplacesEntireBundle(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
ctx := context.Background()
|
||||
|
||||
// Initial set.
|
||||
if _, err := reg.Apply(ctx, g, &messages.SetRecoveryContact{
|
||||
Email: "first@example.com",
|
||||
PushTokens: []*messages.PushToken{
|
||||
{Platform: "fcm", Token: "AAA"},
|
||||
{Platform: "apns", Token: "BBB"},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("first set: %v", err)
|
||||
}
|
||||
if g.Email != "first@example.com" {
|
||||
t.Errorf("email = %q, want first@example.com", g.Email)
|
||||
}
|
||||
if len(g.PushTokens) != 2 {
|
||||
t.Fatalf("push tokens = %d, want 2", len(g.PushTokens))
|
||||
}
|
||||
if g.PushTokens[0].Platform != "fcm" || g.PushTokens[1].Platform != "apns" {
|
||||
t.Errorf("platforms = %+v, want [fcm apns]", g.PushTokens)
|
||||
}
|
||||
|
||||
// Replace with one push token.
|
||||
if _, err := reg.Apply(ctx, g, &messages.SetRecoveryContact{
|
||||
Email: "second@example.com",
|
||||
PushTokens: []*messages.PushToken{{Platform: "webpush", Token: "CCC"}},
|
||||
}); err != nil {
|
||||
t.Fatalf("replace: %v", err)
|
||||
}
|
||||
if g.Email != "second@example.com" {
|
||||
t.Errorf("after replace email = %q, want second@example.com", g.Email)
|
||||
}
|
||||
if len(g.PushTokens) != 1 {
|
||||
t.Fatalf("after replace push tokens = %d, want 1", len(g.PushTokens))
|
||||
}
|
||||
if g.PushTokens[0].Platform != "webpush" {
|
||||
t.Errorf("after replace platform = %q, want webpush", g.PushTokens[0].Platform)
|
||||
}
|
||||
|
||||
// Clear.
|
||||
if _, err := reg.Apply(ctx, g, &messages.SetRecoveryContact{}); err != nil {
|
||||
t.Fatalf("clear: %v", err)
|
||||
}
|
||||
if g.Email != "" || len(g.PushTokens) != 0 {
|
||||
t.Errorf("after clear: email=%q tokens=%d, want empty", g.Email, len(g.PushTokens))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetRecoveryContact_NilPayload verifies defensive nil-input behavior.
|
||||
// We document that nil returns an error (no silent swallow of bad requests).
|
||||
func TestSetRecoveryContact_NilPayload(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(1, time.Now())
|
||||
ctx := context.Background()
|
||||
res, err := reg.Apply(ctx, g, (*messages.SetRecoveryContact)(nil))
|
||||
// registry.Apply treats typed-nil proto messages as a no-op (see
|
||||
// mutation_registry.go's "Typed nil" handling). The behavior we want is
|
||||
// "no mutation applied to grain state" — either via no-op via the
|
||||
// typed-nil path or via an error in the result list. We assert that the
|
||||
// grain state was not modified.
|
||||
if g.Email != "" || len(g.PushTokens) != 0 {
|
||||
t.Errorf("nil-set should not modify grain: email=%q tokens=%d", g.Email, len(g.PushTokens))
|
||||
}
|
||||
_ = res
|
||||
_ = err
|
||||
}
|
||||
|
||||
// TestSetRecoveryContact_SkipsNilTokens verifies nil sub-message tokens are
|
||||
// dropped rather than crashing.
|
||||
func TestSetRecoveryContact_SkipsNilTokens(t *testing.T) {
|
||||
reg := NewCartMultationRegistry(NewCartMutationContext(nil))
|
||||
g := NewCartGrain(2, time.Now())
|
||||
ctx := context.Background()
|
||||
if _, err := reg.Apply(ctx, g, &messages.SetRecoveryContact{
|
||||
Email: "ok@example.com",
|
||||
PushTokens: []*messages.PushToken{
|
||||
nil,
|
||||
{Platform: "fcm", Token: "REAL"},
|
||||
nil,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("set with nil tokens: %v", err)
|
||||
}
|
||||
if len(g.PushTokens) != 1 {
|
||||
t.Fatalf("push tokens = %d, want 1 (nil entries dropped)", len(g.PushTokens))
|
||||
}
|
||||
if g.PushTokens[0].Platform != "fcm" {
|
||||
t.Errorf("kept platform = %q, want fcm", g.PushTokens[0].Platform)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
)
|
||||
|
||||
// LoggingNotifier is the v0 default: it writes one INFO line per candidate
|
||||
// and returns nil. It deliberately does NOT pretend to send anything.
|
||||
//
|
||||
// Future implementations (MailerSend, FCM, APNs) implement the same Notifier
|
||||
// interface; the scanner doesn't change. LoggingNotifier also serves as the
|
||||
// "dry-run" mode in production by leaving the seam unconfigured.
|
||||
type LoggingNotifier struct{}
|
||||
|
||||
// NewLoggingNotifier returns a Notifier that logs every candidate. The struct
|
||||
// is empty today; the constructor is kept for parity with future notifiers
|
||||
// that need credentials / config.
|
||||
func NewLoggingNotifier() *LoggingNotifier { return &LoggingNotifier{} }
|
||||
|
||||
// NotifyAbandoned logs the cart id, contact present, and a tiny summary.
|
||||
// Push tokens are summarized by platform only — never log the raw token, so
|
||||
// a misconfigured log aggregator can't leak device handles.
|
||||
func (LoggingNotifier) NotifyAbandoned(_ context.Context, c RecoveryCandidate) error {
|
||||
hasEmail := c.Email != ""
|
||||
hasPush := len(c.PushTokens) > 0
|
||||
channel := "none"
|
||||
switch {
|
||||
case hasEmail && hasPush:
|
||||
channel = "email+push"
|
||||
case hasEmail:
|
||||
channel = "email"
|
||||
case hasPush:
|
||||
channel = "push"
|
||||
}
|
||||
log.Printf(
|
||||
"recovery [dry-run]: cart=%d user=%q channel=%s items=%d total=%d currency=%s email=%q pushPlatforms=%v lastChangeUnix=%d",
|
||||
c.CartID,
|
||||
c.UserID,
|
||||
channel,
|
||||
c.ItemCount,
|
||||
c.TotalIncVat,
|
||||
c.Currency,
|
||||
c.Email,
|
||||
platformsOf(c.PushTokens),
|
||||
c.LastChangeUnix,
|
||||
)
|
||||
return nil
|
||||
}
|
||||
|
||||
func platformsOf(tokens []cart.PushToken) []string {
|
||||
out := make([]string, 0, len(tokens))
|
||||
for _, t := range tokens {
|
||||
out = append(out, t.Platform)
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"log"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
)
|
||||
|
||||
// captureLog writes log output to an in-memory buffer for inspection.
|
||||
func captureLog(t *testing.T) *bytes.Buffer {
|
||||
t.Helper()
|
||||
buf := &bytes.Buffer{}
|
||||
orig := log.Writer()
|
||||
flags := log.Flags()
|
||||
prefix := log.Prefix()
|
||||
log.SetOutput(buf)
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("")
|
||||
t.Cleanup(func() {
|
||||
log.SetOutput(orig)
|
||||
log.SetFlags(flags)
|
||||
log.SetPrefix(prefix)
|
||||
})
|
||||
return buf
|
||||
}
|
||||
|
||||
func TestLoggingNotifier_LogsPlatformsNotTokens(t *testing.T) {
|
||||
buf := captureLog(t)
|
||||
n := LoggingNotifier{}
|
||||
err := n.NotifyAbandoned(context.Background(), RecoveryCandidate{
|
||||
CartID: 1,
|
||||
Email: "doc@example.com",
|
||||
PushTokens: []cart.PushToken{{Platform: "fcm", Token: "SECRET-TOKEN-MUST-NOT-LEAK"}, {Platform: "apns", Token: "ALSO-SECRET-2"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("NotifyAbandoned: %v", err)
|
||||
}
|
||||
out := buf.String()
|
||||
if !strings.Contains(out, "doc@example.com") {
|
||||
t.Errorf("expected email in log: %s", out)
|
||||
}
|
||||
if !strings.Contains(out, "[fcm apns]") {
|
||||
t.Errorf("expected bracketed platform list in log: %s", out)
|
||||
}
|
||||
for _, leak := range []string{"SECRET-TOKEN-MUST-NOT-LEAK", "ALSO-SECRET-2"} {
|
||||
if strings.Contains(out, leak) {
|
||||
t.Errorf("token leaked into log: %s line=%s", leak, out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoggingNotifier_ClassifiesChannel(t *testing.T) {
|
||||
buf := captureLog(t)
|
||||
n := LoggingNotifier{}
|
||||
cases := []struct {
|
||||
name string
|
||||
cand RecoveryCandidate
|
||||
wantSub string
|
||||
}{
|
||||
{
|
||||
name: "email+push",
|
||||
cand: RecoveryCandidate{Email: "a@b.c", PushTokens: []cart.PushToken{{Platform: "fcm"}}},
|
||||
wantSub: "channel=email+push",
|
||||
},
|
||||
{
|
||||
name: "email only",
|
||||
cand: RecoveryCandidate{Email: "a@b.c"},
|
||||
wantSub: "channel=email",
|
||||
},
|
||||
{
|
||||
name: "push only",
|
||||
cand: RecoveryCandidate{PushTokens: []cart.PushToken{{Platform: "fcm"}}},
|
||||
wantSub: "channel=push",
|
||||
},
|
||||
{
|
||||
name: "no contact",
|
||||
cand: RecoveryCandidate{},
|
||||
wantSub: "channel=none",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
buf.Reset()
|
||||
if err := n.NotifyAbandoned(context.Background(), tc.cand); err != nil {
|
||||
t.Fatalf("%s: %v", tc.name, err)
|
||||
}
|
||||
if !strings.Contains(buf.String(), tc.wantSub) {
|
||||
t.Errorf("%s: log missing %q; got: %s", tc.name, tc.wantSub, buf.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// Package recovery is the v0 abandoned-cart-recovery seam.
|
||||
//
|
||||
// The contract is intentionally narrow: a Scanner finds candidates whose last
|
||||
// mutation is within an "abandoned" window and that have BOTH an attachable
|
||||
// contact (email and/or push tokens) and items in the cart, then hands them
|
||||
// to a Notifier. The Notifier decides what to do — for now a logging shim.
|
||||
//
|
||||
// The seam intentionally avoids:
|
||||
//
|
||||
// - Email/push-token delivery (lives in the notifier impl).
|
||||
// - Idempotency tracking ("did we already notify?"). v0: scanner windows
|
||||
// are sized so a cart falls in once per detection threshold. Real
|
||||
// idempotency is the next pass — see suggest_followups / plan-commerce-
|
||||
// maturity C3.
|
||||
// - Cross-pod coordination. Each cart pod owns its own shard of event logs
|
||||
// (replicated read cache, no leader), so per-pod scanning is correct.
|
||||
// A future AMQP fan-out of `cart.recovery.candidate` would let a single
|
||||
// dedicated notifier consume instead — out of scope here.
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
)
|
||||
|
||||
// RecoveryCandidate is the projectable "we'd remind this shopper" shape.
|
||||
// Fields surface just enough for a future email template / push payload; the
|
||||
// underlying grain carries everything else.
|
||||
type RecoveryCandidate struct {
|
||||
CartID cart.CartId
|
||||
UserID string // empty if the cart hasn't been linked to a profile
|
||||
Email string
|
||||
PushTokens []cart.PushToken
|
||||
ItemCount int
|
||||
TotalIncVat int64 // minor units (money.Cents == int64)
|
||||
Currency string
|
||||
// LastChange is best-effort: the scanner reads the event-log file's
|
||||
// modification time. A pending notifier shouldn't use it as the canonical
|
||||
// "when was this cart touched?" wall clock — see cart.LastChange.
|
||||
LastChangeUnix int64
|
||||
}
|
||||
|
||||
// Notifier is the seam a real email/push implementation plugs into. The
|
||||
// LoggingNotifier is the v0 default. The cart service ships the seam even
|
||||
// before a real send exists so call-sites and tracking can be wired once.
|
||||
type Notifier interface {
|
||||
NotifyAbandoned(ctx context.Context, c RecoveryCandidate) error
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// writeFixtureLog writes a synthetic `<id>.events.log` to dir with the given
|
||||
// mutations, then pins the file's mtime AFTER Close so the canonical
|
||||
// DiskStorage.SaveLoop flush can't overwrite it on test machines that run
|
||||
// the inner save() during teardown.
|
||||
func writeFixtureLog(t *testing.T, dir string, id uint64, mtime time.Time, muts []proto.Message) {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, strconv.FormatUint(id, 10)+".events.log")
|
||||
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
storage := actor.NewDiskStorage[cart.CartGrain](dir, reg)
|
||||
if err := storage.AppendMutations(id, muts...); err != nil {
|
||||
t.Fatalf("AppendMutations for fixture %d: %v", id, err)
|
||||
}
|
||||
// DiskStorage.Close runs save() which opens+appends the file; that
|
||||
// updates mtime. Pin AFTER Close so the test mtime wins.
|
||||
storage.Close()
|
||||
if err := os.Chtimes(path, mtime, mtime); err != nil {
|
||||
t.Fatalf("Chtimes %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
// silenceLogs redirects the standard logger while the scanner runs so test
|
||||
// output stays readable.
|
||||
func silenceLogs(t *testing.T) {
|
||||
t.Helper()
|
||||
orig := log.Writer()
|
||||
prevFlags := log.Flags()
|
||||
log.SetOutput(devNull{})
|
||||
log.SetFlags(0)
|
||||
t.Cleanup(func() {
|
||||
log.SetOutput(orig)
|
||||
log.SetFlags(prevFlags)
|
||||
})
|
||||
}
|
||||
|
||||
type devNull struct{}
|
||||
|
||||
func (devNull) Write(p []byte) (int, error) { return len(p), nil }
|
||||
|
||||
func TestScanner_FindsCandidateInWindow(t *testing.T) {
|
||||
silenceLogs(t)
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
storage := actor.NewDiskStorage[cart.CartGrain](dir, reg)
|
||||
|
||||
// Cart that became eligible exactly in the middle of the window:
|
||||
// delay 1h, window 1h → eligible mtime ∈ [now-2h, now-1h].
|
||||
mtime := now.Add(-90 * time.Minute)
|
||||
writeFixtureLog(t, dir, 42, mtime, []proto.Message{
|
||||
&messages.AddItem{ItemId: 1, Quantity: 1, Price: 1000, Sku: "A"},
|
||||
&messages.SetRecoveryContact{Email: "abandoned@example.com"},
|
||||
})
|
||||
defer storage.Close()
|
||||
|
||||
scanner := NewScanner(dir, storage)
|
||||
got, err := scanner.Scan(context.Background(), time.Hour, time.Hour, now)
|
||||
if err != nil {
|
||||
t.Fatalf("Scan: %v", err)
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("candidates = %d, want 1; full result=%+v", len(got), got)
|
||||
}
|
||||
c := got[0]
|
||||
if c.CartID != cart.CartId(42) {
|
||||
t.Errorf("CartID = %d, want 42", c.CartID)
|
||||
}
|
||||
if c.Email != "abandoned@example.com" {
|
||||
t.Errorf("Email = %q, want abandoned@example.com", c.Email)
|
||||
}
|
||||
if c.ItemCount != 1 {
|
||||
t.Errorf("ItemCount = %d, want 1", c.ItemCount)
|
||||
}
|
||||
if c.TotalIncVat != 1000 {
|
||||
t.Errorf("TotalIncVat = %d, want 1000 (AddItem price 1000)", c.TotalIncVat)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanner_SkipsCartsOutsideWindow(t *testing.T) {
|
||||
silenceLogs(t)
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
storage := actor.NewDiskStorage[cart.CartGrain](dir, reg)
|
||||
|
||||
// Too fresh (mtime = now-30m, inside the 1h "still active" cutoff).
|
||||
fresh := now.Add(-30 * time.Minute)
|
||||
writeFixtureLog(t, dir, 7, fresh, []proto.Message{
|
||||
&messages.AddItem{ItemId: 1, Quantity: 1, Price: 1000},
|
||||
&messages.SetRecoveryContact{Email: "fresh@example.com"},
|
||||
})
|
||||
// Too old (mtime = now-25h, past the 2h window upper bound).
|
||||
old := now.Add(-25 * time.Hour)
|
||||
writeFixtureLog(t, dir, 8, old, []proto.Message{
|
||||
&messages.AddItem{ItemId: 1, Quantity: 1, Price: 1000},
|
||||
&messages.SetRecoveryContact{Email: "old@example.com"},
|
||||
})
|
||||
// Inside window with NO contact (eligibility gate).
|
||||
mid := now.Add(-90 * time.Minute)
|
||||
writeFixtureLog(t, dir, 9, mid, []proto.Message{
|
||||
&messages.AddItem{ItemId: 1, Quantity: 1, Price: 1000},
|
||||
// no SetRecoveryContact
|
||||
})
|
||||
defer storage.Close()
|
||||
|
||||
scanner := NewScanner(dir, storage)
|
||||
got, err := scanner.Scan(context.Background(), time.Hour, time.Hour, now)
|
||||
if err != nil {
|
||||
t.Fatalf("Scan: %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("candidates = %d, want 0 (all filtered out); result=%+v", len(got), got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanner_FindsPushOnlyCandidate(t *testing.T) {
|
||||
silenceLogs(t)
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
storage := actor.NewDiskStorage[cart.CartGrain](dir, reg)
|
||||
|
||||
mtime := now.Add(-75 * time.Minute)
|
||||
writeFixtureLog(t, dir, 11, mtime, []proto.Message{
|
||||
&messages.AddItem{ItemId: 1, Quantity: 1, Price: 1000},
|
||||
&messages.SetRecoveryContact{
|
||||
PushTokens: []*messages.PushToken{
|
||||
{Platform: "fcm", Token: "tok-1"},
|
||||
{Platform: "apns", Token: "tok-2"},
|
||||
},
|
||||
},
|
||||
})
|
||||
defer storage.Close()
|
||||
|
||||
scanner := NewScanner(dir, storage)
|
||||
got, err := scanner.Scan(context.Background(), time.Hour, time.Hour, now)
|
||||
if err != nil {
|
||||
t.Fatalf("Scan: %v", err)
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("candidates = %d, want 1", len(got))
|
||||
}
|
||||
if got[0].Email != "" {
|
||||
t.Errorf("Email = %q, want empty (push-only)", got[0].Email)
|
||||
}
|
||||
if len(got[0].PushTokens) != 2 {
|
||||
t.Fatalf("PushTokens = %d, want 2", len(got[0].PushTokens))
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanner_IgnoresWishlistCarts(t *testing.T) {
|
||||
silenceLogs(t)
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
storage := actor.NewDiskStorage[cart.CartGrain](dir, reg)
|
||||
|
||||
mtime := now.Add(-90 * time.Minute)
|
||||
writeFixtureLog(t, dir, 13, mtime, []proto.Message{
|
||||
&messages.AddItem{ItemId: 1, Quantity: 1, Price: 1000},
|
||||
&messages.SetRecoveryContact{Email: "wishlist@example.com"},
|
||||
&messages.SetCartType{Type: messages.CartType_WISHLIST},
|
||||
})
|
||||
defer storage.Close()
|
||||
|
||||
scanner := NewScanner(dir, storage)
|
||||
got, err := scanner.Scan(context.Background(), time.Hour, time.Hour, now)
|
||||
if err != nil {
|
||||
t.Fatalf("Scan: %v", err)
|
||||
}
|
||||
if len(got) != 0 {
|
||||
t.Fatalf("wishlist candidate leaked: got %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// TestScanner_TotalPostReplay verifies the bug fix: replay does not run the
|
||||
// post-mutation processor, so the scanner MUST call UpdateTotals to surface
|
||||
// an accurate TotalIncVat.
|
||||
func TestScanner_TotalPostReplay(t *testing.T) {
|
||||
silenceLogs(t)
|
||||
dir := t.TempDir()
|
||||
now := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
reg := cart.NewCartMultationRegistry(cart.NewCartMutationContext(nil))
|
||||
storage := actor.NewDiskStorage[cart.CartGrain](dir, reg)
|
||||
|
||||
mtime := now.Add(-75 * time.Minute)
|
||||
writeFixtureLog(t, dir, 21, mtime, []proto.Message{
|
||||
&messages.AddItem{ItemId: 1, Quantity: 2, Price: 500, Sku: "A"},
|
||||
&messages.AddItem{ItemId: 2, Quantity: 1, Price: 1500, Sku: "B"},
|
||||
&messages.SetRecoveryContact{Email: "totals@example.com"},
|
||||
})
|
||||
defer storage.Close()
|
||||
|
||||
scanner := NewScanner(dir, storage)
|
||||
got, err := scanner.Scan(context.Background(), time.Hour, time.Hour, now)
|
||||
if err != nil {
|
||||
t.Fatalf("Scan: %v", err)
|
||||
}
|
||||
if len(got) != 1 {
|
||||
t.Fatalf("candidates = %d, want 1", len(got))
|
||||
}
|
||||
// 2*500 + 1*1500 = 2500 inc-vat
|
||||
if got[0].TotalIncVat != 2500 {
|
||||
t.Errorf("TotalIncVat = %d, want 2500 (1000+1500 across two lines)", got[0].TotalIncVat)
|
||||
}
|
||||
if got[0].ItemCount != 2 {
|
||||
t.Errorf("ItemCount = %d, want 2", got[0].ItemCount)
|
||||
}
|
||||
}
|
||||
|
||||
// TestIsValidEmail sanity for the optional helper exported for notifiers.
|
||||
func TestIsValidEmail(t *testing.T) {
|
||||
if !IsValidEmail("user@example.com") {
|
||||
t.Errorf("user@example.com should be valid")
|
||||
}
|
||||
if IsValidEmail("not-an-email") {
|
||||
t.Errorf("not-an-email should be invalid")
|
||||
}
|
||||
}
|
||||
|
||||
// TestFileIDFor verifies the helper that parses <id>.events.log filenames.
|
||||
func TestFileIDFor(t *testing.T) {
|
||||
if id, ok := fileIDFor("12345.events.log"); !ok || id != 12345 {
|
||||
t.Errorf("12345.events.log -> %d, %v; want 12345, true", id, ok)
|
||||
}
|
||||
if _, ok := fileIDFor("not-a-number.events.log"); ok {
|
||||
t.Errorf("non-numeric should fail")
|
||||
}
|
||||
if _, ok := fileIDFor("0.events.log"); ok {
|
||||
t.Errorf("id=0 should fail (no implicit zero-cart grain)")
|
||||
}
|
||||
if _, ok := fileIDFor("garbage.txt"); ok {
|
||||
t.Errorf("non-event-log suffix should fail")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/mail"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/actor"
|
||||
"git.k6n.net/mats/go-cart-actor/pkg/cart"
|
||||
cart_messages "git.k6n.net/mats/go-cart-actor/proto/cart"
|
||||
)
|
||||
|
||||
// Scanner finds RecoveryCandidates by scanning the per-pod event-log directory.
|
||||
//
|
||||
// The model:
|
||||
// - Each cart pod owns a shard of `<id>.events.log` files under `dir`.
|
||||
// - File mtime approximates "last successful mutation" cheaply — replay only
|
||||
// when something lands in the window.
|
||||
// - For each candidate file, replay the grain (a few kb, ms-scale) and build
|
||||
// a RecoveryCandidate iff it has items + a contact.
|
||||
//
|
||||
// Two important implementation details:
|
||||
//
|
||||
// 1. After LoadEvents we MUST call UpdateTotals(). Replaying event handlers
|
||||
// runs the per-mutation registered handlers (SetUserId, SetRecoveryContact,
|
||||
// AddItem, ...) but NOT the post-mutation processors — those run only on
|
||||
// the live mutation path (see cmd/cart/main.go::RegisterProcessor for the
|
||||
// "Totals and promotions" pipeline). Without this call, g.TotalPrice is
|
||||
// nil and TotalIncVat in the candidate is always 0.
|
||||
//
|
||||
// 2. Read-vs-write races: DiskStorage may be in the middle of appending while
|
||||
// Scan reads. The bufio scanner will fail JSON-unmarshal on a torn line and
|
||||
// we silently continue — LogEvents treats that as "skip", here we do too.
|
||||
// A persistent unreadable file is a separate problem runPurge handles.
|
||||
//
|
||||
// v0 deliberately does NOT track "already notified" within this scanner —
|
||||
// the window sizing plus caller conventions give roughly-once delivery.
|
||||
// Idempotency is the next pass.
|
||||
type Scanner struct {
|
||||
dir string
|
||||
storage *actor.DiskStorage[cart.CartGrain]
|
||||
}
|
||||
|
||||
// NewScanner returns a Scanner over dir (the same CART_DIR used by the cart
|
||||
// service). storage must share the same dir so LoadEvents reading paths
|
||||
// match the scanning paths.
|
||||
func NewScanner(dir string, storage *actor.DiskStorage[cart.CartGrain]) *Scanner {
|
||||
return &Scanner{dir: dir, storage: storage}
|
||||
}
|
||||
|
||||
// Scan returns candidates whose event-log mtime falls in
|
||||
// [now-delay-window, now-delay] AND whose grain has items AND (email OR
|
||||
// push tokens). The half-open window semantics match the caller's intent:
|
||||
//
|
||||
// "carts whose mtime is in this slice of the recent past"
|
||||
//
|
||||
// — younger than now-delay (still active) and older than now-delay-window
|
||||
// (never recovery-eligible yet).
|
||||
//
|
||||
// Empty slice + nil error means "no candidates this pass"; an error means the
|
||||
// pass itself failed and the caller should log + retry.
|
||||
func (s *Scanner) Scan(ctx context.Context, delay, window time.Duration, now time.Time) ([]RecoveryCandidate, error) {
|
||||
if s == nil || s.storage == nil || s.dir == "" {
|
||||
return nil, fmt.Errorf("recovery: scanner not configured")
|
||||
}
|
||||
if delay <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if window <= 0 {
|
||||
window = delay
|
||||
}
|
||||
|
||||
lower := now.Add(-(delay + window))
|
||||
upper := now.Add(-delay)
|
||||
entries, err := os.ReadDir(s.dir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("recovery: read dir %s: %w", s.dir, err)
|
||||
}
|
||||
|
||||
out := make([]RecoveryCandidate, 0)
|
||||
for _, e := range entries {
|
||||
if e.IsDir() {
|
||||
continue
|
||||
}
|
||||
name := e.Name()
|
||||
if !strings.HasSuffix(name, ".events.log") {
|
||||
continue
|
||||
}
|
||||
id, ok := fileIDFor(name)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
info, infoErr := e.Info()
|
||||
if infoErr != nil {
|
||||
continue
|
||||
}
|
||||
mtime := info.ModTime()
|
||||
// Closed window:
|
||||
// mtime BEFORE lower → never reached the abandonment threshold yet.
|
||||
// mtime AFTER upper → still too fresh (caller is actively using it).
|
||||
if mtime.Before(lower) || mtime.After(upper) {
|
||||
continue
|
||||
}
|
||||
|
||||
grain := cart.NewCartGrain(id, mtime)
|
||||
if loadErr := s.storage.LoadEvents(ctx, id, grain); loadErr != nil {
|
||||
// Skip unreadable grains — they're purged separately by runPurge.
|
||||
continue
|
||||
}
|
||||
// Replay does NOT run the Totals-and-promotions processor (see the
|
||||
// package doc); recompute TotalPrice from the item list so the
|
||||
// candidate TotalIncVat matches what the live API would return.
|
||||
grain.UpdateTotals()
|
||||
if !eligible(grain) {
|
||||
continue
|
||||
}
|
||||
out = append(out, buildCandidate(id, grain, mtime))
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// eligible mirrors "would we ever want to send a reminder?" — items present
|
||||
// and a contact available. The grain's own UpdateTotals has already been
|
||||
// run by every mutation's processor, so TotalPrice is reliable here.
|
||||
//
|
||||
// Wishlists / offers are excluded — a wishlist cart isn't forgotten, and an
|
||||
// offer cart has its own delivery semantics.
|
||||
func eligible(g *cart.CartGrain) bool {
|
||||
if g == nil || len(g.Items) == 0 {
|
||||
return false
|
||||
}
|
||||
switch g.Type {
|
||||
case cart_messages.CartType_WISHLIST, cart_messages.CartType_OFFER:
|
||||
return false
|
||||
}
|
||||
hasContact := strings.TrimSpace(g.Email) != "" || len(g.PushTokens) > 0
|
||||
return hasContact
|
||||
}
|
||||
|
||||
// buildCandidate projects the replayed grain into a RecoveryCandidate. Email
|
||||
// validation is intentionally lenient — we hand the value to the notifier
|
||||
// and the notifier decides what to send; rejecting "looks-invalid" addresses
|
||||
// here would just hide useful debugging signal from the dry-run logs.
|
||||
func buildCandidate(id uint64, g *cart.CartGrain, mtime time.Time) RecoveryCandidate {
|
||||
var total int64
|
||||
if g.TotalPrice != nil {
|
||||
// tax.Price.IncVat is int64-backed via money.Cents. Reading the field
|
||||
// directly keeps the recovery layer decoupled from platform/money
|
||||
// internals.
|
||||
total = int64(g.TotalPrice.IncVat)
|
||||
}
|
||||
tokens := make([]cart.PushToken, len(g.PushTokens))
|
||||
copy(tokens, g.PushTokens)
|
||||
return RecoveryCandidate{
|
||||
CartID: cart.CartId(id),
|
||||
UserID: g.UserId,
|
||||
Email: g.Email,
|
||||
PushTokens: tokens,
|
||||
ItemCount: len(g.Items),
|
||||
TotalIncVat: total,
|
||||
Currency: g.Currency,
|
||||
LastChangeUnix: mtime.Unix(),
|
||||
}
|
||||
}
|
||||
|
||||
// IsValidEmail is exported as a convenience the notifier may use to decide
|
||||
// whether to actually send (MailerSend will reject invalid); the scanner
|
||||
// itself is permissive.
|
||||
func IsValidEmail(s string) bool {
|
||||
_, err := mail.ParseAddress(s)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// fileIDFor is a small helper that extracts the grain id from an .events.log
|
||||
// filename. Returns 0/false if the name doesn't match the expected shape.
|
||||
func fileIDFor(name string) (uint64, bool) {
|
||||
base := strings.TrimSuffix(name, ".events.log")
|
||||
id, err := strconv.ParseUint(base, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
@@ -105,6 +105,25 @@ message SetCartType {
|
||||
CartType type = 1;
|
||||
}
|
||||
|
||||
// PushToken is a generic, provider-agnostic push-delivery target. The cart layer
|
||||
// only stores what it was given — actual delivery (FCM, APNs, Web Push) is the
|
||||
// notifier's job (see pkg/cart/recovery.LoggingNotifier for the v0 contract).
|
||||
//
|
||||
// Platform is free-form (e.g. "fcm", "apns", "webpush") so callers can record
|
||||
// multiple devices per cart without forcing a schema decision here.
|
||||
message PushToken {
|
||||
string platform = 1;
|
||||
string token = 2;
|
||||
}
|
||||
|
||||
// SetRecoveryContact attaches the contact bundle used by the abandoned-cart
|
||||
// recovery flow. PUT-style: replaces the entire contact in one event — empty
|
||||
// strings or empty token lists are persisted as "no contact".
|
||||
message SetRecoveryContact {
|
||||
string email = 1;
|
||||
repeated PushToken push_tokens = 2;
|
||||
}
|
||||
|
||||
message Mutation {
|
||||
oneof type {
|
||||
ClearCartRequest clear_cart = 1;
|
||||
@@ -120,6 +139,7 @@ message Mutation {
|
||||
RemoveVoucher remove_voucher = 21;
|
||||
UpsertSubscriptionDetails upsert_subscription_details = 22;
|
||||
SetCartType set_cart_type = 23;
|
||||
SetRecoveryContact set_recovery_contact = 24;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+290
-240
@@ -1,6 +1,6 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.5
|
||||
// protoc-gen-go v1.36.11
|
||||
// protoc v7.35.1
|
||||
// source: cart.proto
|
||||
|
||||
@@ -971,6 +971,119 @@ func (x *SetCartType) GetType() CartType {
|
||||
return CartType_REGULAR
|
||||
}
|
||||
|
||||
// PushToken is a generic, provider-agnostic push-delivery target. The cart layer
|
||||
// only stores what it was given — actual delivery (FCM, APNs, Web Push) is the
|
||||
// notifier's job (see pkg/cart/recovery.LoggingNotifier for the v0 contract).
|
||||
//
|
||||
// Platform is free-form (e.g. "fcm", "apns", "webpush") so callers can record
|
||||
// multiple devices per cart without forcing a schema decision here.
|
||||
type PushToken struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Platform string `protobuf:"bytes,1,opt,name=platform,proto3" json:"platform,omitempty"`
|
||||
Token string `protobuf:"bytes,2,opt,name=token,proto3" json:"token,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PushToken) Reset() {
|
||||
*x = PushToken{}
|
||||
mi := &file_cart_proto_msgTypes[13]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PushToken) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PushToken) ProtoMessage() {}
|
||||
|
||||
func (x *PushToken) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_cart_proto_msgTypes[13]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PushToken.ProtoReflect.Descriptor instead.
|
||||
func (*PushToken) Descriptor() ([]byte, []int) {
|
||||
return file_cart_proto_rawDescGZIP(), []int{13}
|
||||
}
|
||||
|
||||
func (x *PushToken) GetPlatform() string {
|
||||
if x != nil {
|
||||
return x.Platform
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *PushToken) GetToken() string {
|
||||
if x != nil {
|
||||
return x.Token
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// SetRecoveryContact attaches the contact bundle used by the abandoned-cart
|
||||
// recovery flow. PUT-style: replaces the entire contact in one event — empty
|
||||
// strings or empty token lists are persisted as "no contact".
|
||||
type SetRecoveryContact struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Email string `protobuf:"bytes,1,opt,name=email,proto3" json:"email,omitempty"`
|
||||
PushTokens []*PushToken `protobuf:"bytes,2,rep,name=push_tokens,json=pushTokens,proto3" json:"push_tokens,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *SetRecoveryContact) Reset() {
|
||||
*x = SetRecoveryContact{}
|
||||
mi := &file_cart_proto_msgTypes[14]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *SetRecoveryContact) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*SetRecoveryContact) ProtoMessage() {}
|
||||
|
||||
func (x *SetRecoveryContact) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_cart_proto_msgTypes[14]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use SetRecoveryContact.ProtoReflect.Descriptor instead.
|
||||
func (*SetRecoveryContact) Descriptor() ([]byte, []int) {
|
||||
return file_cart_proto_rawDescGZIP(), []int{14}
|
||||
}
|
||||
|
||||
func (x *SetRecoveryContact) GetEmail() string {
|
||||
if x != nil {
|
||||
return x.Email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *SetRecoveryContact) GetPushTokens() []*PushToken {
|
||||
if x != nil {
|
||||
return x.PushTokens
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Mutation struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
// Types that are valid to be assigned to Type:
|
||||
@@ -988,6 +1101,7 @@ type Mutation struct {
|
||||
// *Mutation_RemoveVoucher
|
||||
// *Mutation_UpsertSubscriptionDetails
|
||||
// *Mutation_SetCartType
|
||||
// *Mutation_SetRecoveryContact
|
||||
Type isMutation_Type `protobuf_oneof:"type"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -995,7 +1109,7 @@ type Mutation struct {
|
||||
|
||||
func (x *Mutation) Reset() {
|
||||
*x = Mutation{}
|
||||
mi := &file_cart_proto_msgTypes[13]
|
||||
mi := &file_cart_proto_msgTypes[15]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
@@ -1007,7 +1121,7 @@ func (x *Mutation) String() string {
|
||||
func (*Mutation) ProtoMessage() {}
|
||||
|
||||
func (x *Mutation) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_cart_proto_msgTypes[13]
|
||||
mi := &file_cart_proto_msgTypes[15]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
@@ -1020,7 +1134,7 @@ func (x *Mutation) ProtoReflect() protoreflect.Message {
|
||||
|
||||
// Deprecated: Use Mutation.ProtoReflect.Descriptor instead.
|
||||
func (*Mutation) Descriptor() ([]byte, []int) {
|
||||
return file_cart_proto_rawDescGZIP(), []int{13}
|
||||
return file_cart_proto_rawDescGZIP(), []int{15}
|
||||
}
|
||||
|
||||
func (x *Mutation) GetType() isMutation_Type {
|
||||
@@ -1147,6 +1261,15 @@ func (x *Mutation) GetSetCartType() *SetCartType {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *Mutation) GetSetRecoveryContact() *SetRecoveryContact {
|
||||
if x != nil {
|
||||
if x, ok := x.Type.(*Mutation_SetRecoveryContact); ok {
|
||||
return x.SetRecoveryContact
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type isMutation_Type interface {
|
||||
isMutation_Type()
|
||||
}
|
||||
@@ -1203,6 +1326,10 @@ type Mutation_SetCartType struct {
|
||||
SetCartType *SetCartType `protobuf:"bytes,23,opt,name=set_cart_type,json=setCartType,proto3,oneof"`
|
||||
}
|
||||
|
||||
type Mutation_SetRecoveryContact struct {
|
||||
SetRecoveryContact *SetRecoveryContact `protobuf:"bytes,24,opt,name=set_recovery_contact,json=setRecoveryContact,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (*Mutation_ClearCart) isMutation_Type() {}
|
||||
|
||||
func (*Mutation_AddItem) isMutation_Type() {}
|
||||
@@ -1229,214 +1356,132 @@ func (*Mutation_UpsertSubscriptionDetails) isMutation_Type() {}
|
||||
|
||||
func (*Mutation_SetCartType) isMutation_Type() {}
|
||||
|
||||
func (*Mutation_SetRecoveryContact) isMutation_Type() {}
|
||||
|
||||
var File_cart_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_cart_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x0a, 0x63, 0x61, 0x72, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x63, 0x61,
|
||||
0x72, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x1a, 0x19, 0x67, 0x6f, 0x6f,
|
||||
0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79,
|
||||
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70,
|
||||
0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
|
||||
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x12, 0x0a, 0x10, 0x43, 0x6c, 0x65, 0x61, 0x72,
|
||||
0x43, 0x61, 0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22, 0xaa, 0x08, 0x0a, 0x07,
|
||||
0x41, 0x64, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x69, 0x74, 0x65, 0x6d, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x69, 0x74, 0x65, 0x6d, 0x49, 0x64,
|
||||
0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x05, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05,
|
||||
0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x70, 0x72, 0x69,
|
||||
0x63, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x6f, 0x72, 0x67, 0x50, 0x72, 0x69, 0x63, 0x65, 0x18, 0x09,
|
||||
0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6f, 0x72, 0x67, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x10,
|
||||
0x0a, 0x03, 0x73, 0x6b, 0x75, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x73, 0x6b, 0x75,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74,
|
||||
0x6f, 0x63, 0x6b, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x73, 0x74, 0x6f, 0x63, 0x6b,
|
||||
0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x05, 0x52, 0x03, 0x74,
|
||||
0x61, 0x78, 0x12, 0x14, 0x0a, 0x05, 0x62, 0x72, 0x61, 0x6e, 0x64, 0x18, 0x0d, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x05, 0x62, 0x72, 0x61, 0x6e, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x61, 0x74, 0x65,
|
||||
0x67, 0x6f, 0x72, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x74, 0x65,
|
||||
0x67, 0x6f, 0x72, 0x79, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79,
|
||||
0x32, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72,
|
||||
0x79, 0x32, 0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x33, 0x18,
|
||||
0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x33,
|
||||
0x12, 0x1c, 0x0a, 0x09, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x34, 0x18, 0x11, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x34, 0x12, 0x1c,
|
||||
0x0a, 0x09, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x35, 0x18, 0x12, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x09, 0x63, 0x61, 0x74, 0x65, 0x67, 0x6f, 0x72, 0x79, 0x35, 0x12, 0x1e, 0x0a, 0x0a,
|
||||
0x64, 0x69, 0x73, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x65, 0x72, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x0a, 0x64, 0x69, 0x73, 0x63, 0x6c, 0x61, 0x69, 0x6d, 0x65, 0x72, 0x12, 0x20, 0x0a, 0x0b,
|
||||
0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x0b, 0x61, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1a,
|
||||
0x0a, 0x08, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x49, 0x64, 0x18, 0x13, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x08, 0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x65,
|
||||
0x6c, 0x6c, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
|
||||
0x73, 0x65, 0x6c, 0x6c, 0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x63, 0x6f,
|
||||
0x75, 0x6e, 0x74, 0x72, 0x79, 0x18, 0x15, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x63, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x12, 0x1e, 0x0a, 0x0a, 0x73, 0x61, 0x6c, 0x65, 0x53, 0x74, 0x61, 0x74,
|
||||
0x75, 0x73, 0x18, 0x18, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x73, 0x61, 0x6c, 0x65, 0x53, 0x74,
|
||||
0x61, 0x74, 0x75, 0x73, 0x12, 0x1b, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x6c, 0x65, 0x74, 0x18, 0x0c,
|
||||
0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x6c, 0x65, 0x74, 0x88, 0x01,
|
||||
0x01, 0x12, 0x1d, 0x0a, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x64, 0x18, 0x16, 0x20, 0x01,
|
||||
0x28, 0x09, 0x48, 0x01, 0x52, 0x07, 0x73, 0x74, 0x6f, 0x72, 0x65, 0x49, 0x64, 0x88, 0x01, 0x01,
|
||||
0x12, 0x1f, 0x0a, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x17, 0x20, 0x01,
|
||||
0x28, 0x0d, 0x48, 0x02, 0x52, 0x08, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x88, 0x01,
|
||||
0x01, 0x12, 0x10, 0x0a, 0x03, 0x63, 0x67, 0x6d, 0x18, 0x19, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
|
||||
0x63, 0x67, 0x6d, 0x12, 0x4f, 0x0a, 0x12, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75,
|
||||
0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x03, 0x52, 0x12, 0x72,
|
||||
0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x45, 0x6e, 0x64, 0x54, 0x69, 0x6d,
|
||||
0x65, 0x88, 0x01, 0x01, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x78, 0x74, 0x72, 0x61, 0x5f, 0x6a, 0x73,
|
||||
0x6f, 0x6e, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x65, 0x78, 0x74, 0x72, 0x61, 0x4a,
|
||||
0x73, 0x6f, 0x6e, 0x12, 0x4d, 0x0a, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x66, 0x69,
|
||||
0x65, 0x6c, 0x64, 0x73, 0x18, 0x1c, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x61, 0x72,
|
||||
0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x74,
|
||||
0x65, 0x6d, 0x2e, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x52, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c,
|
||||
0x64, 0x73, 0x12, 0x2b, 0x0a, 0x11, 0x69, 0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x5f,
|
||||
0x74, 0x72, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x69,
|
||||
0x6e, 0x76, 0x65, 0x6e, 0x74, 0x6f, 0x72, 0x79, 0x54, 0x72, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x12,
|
||||
0x1b, 0x0a, 0x09, 0x64, 0x72, 0x6f, 0x70, 0x5f, 0x73, 0x68, 0x69, 0x70, 0x18, 0x1e, 0x20, 0x01,
|
||||
0x28, 0x08, 0x52, 0x08, 0x64, 0x72, 0x6f, 0x70, 0x53, 0x68, 0x69, 0x70, 0x1a, 0x3f, 0x0a, 0x11,
|
||||
0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72,
|
||||
0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03,
|
||||
0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x09, 0x0a,
|
||||
0x07, 0x5f, 0x6f, 0x75, 0x74, 0x6c, 0x65, 0x74, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x73, 0x74, 0x6f,
|
||||
0x72, 0x65, 0x49, 0x64, 0x42, 0x0b, 0x0a, 0x09, 0x5f, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x49,
|
||||
0x64, 0x42, 0x15, 0x0a, 0x13, 0x5f, 0x72, 0x65, 0x73, 0x65, 0x72, 0x76, 0x61, 0x74, 0x69, 0x6f,
|
||||
0x6e, 0x45, 0x6e, 0x64, 0x54, 0x69, 0x6d, 0x65, 0x22, 0x1c, 0x0a, 0x0a, 0x52, 0x65, 0x6d, 0x6f,
|
||||
0x76, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01,
|
||||
0x28, 0x0d, 0x52, 0x02, 0x49, 0x64, 0x22, 0x3c, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65,
|
||||
0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x0e, 0x0a, 0x02, 0x49, 0x64, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x71, 0x75, 0x61, 0x6e,
|
||||
0x74, 0x69, 0x74, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x71, 0x75, 0x61, 0x6e,
|
||||
0x74, 0x69, 0x74, 0x79, 0x22, 0x23, 0x0a, 0x09, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49,
|
||||
0x64, 0x12, 0x16, 0x0a, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x22, 0x4f, 0x0a, 0x0f, 0x4c, 0x69, 0x6e,
|
||||
0x65, 0x49, 0x74, 0x65, 0x6d, 0x4d, 0x61, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x12, 0x0e, 0x0a, 0x02,
|
||||
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12, 0x12, 0x0a, 0x04,
|
||||
0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
|
||||
0x12, 0x18, 0x0a, 0x07, 0x6d, 0x61, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x09, 0x52, 0x07, 0x6d, 0x61, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x22, 0x27, 0x0a, 0x15, 0x52, 0x65,
|
||||
0x6d, 0x6f, 0x76, 0x65, 0x4c, 0x69, 0x6e, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x4d, 0x61, 0x72, 0x6b,
|
||||
0x69, 0x6e, 0x67, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52,
|
||||
0x02, 0x69, 0x64, 0x22, 0xc9, 0x01, 0x0a, 0x17, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x65, 0x49,
|
||||
0x74, 0x65, 0x6d, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12,
|
||||
0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69, 0x64, 0x12,
|
||||
0x5d, 0x0a, 0x0d, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73,
|
||||
0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x38, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x65,
|
||||
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x65, 0x49, 0x74,
|
||||
0x65, 0x6d, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x2e, 0x43,
|
||||
0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79,
|
||||
0x52, 0x0c, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x1a, 0x3f,
|
||||
0x0a, 0x11, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x45, 0x6e,
|
||||
0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
|
||||
0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22,
|
||||
0x71, 0x0a, 0x11, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x41,
|
||||
0x64, 0x64, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x69, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x0d, 0x52, 0x06, 0x69, 0x74, 0x65, 0x6d, 0x49, 0x64, 0x12, 0x1c, 0x0a, 0x09,
|
||||
0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||
0x09, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x6f, 0x72,
|
||||
0x64, 0x65, 0x72, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01,
|
||||
0x28, 0x09, 0x52, 0x0e, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e,
|
||||
0x63, 0x65, 0x22, 0x7c, 0x0a, 0x0a, 0x41, 0x64, 0x64, 0x56, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72,
|
||||
0x12, 0x12, 0x0a, 0x04, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
|
||||
0x63, 0x6f, 0x64, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20,
|
||||
0x01, 0x28, 0x03, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x76, 0x6f,
|
||||
0x75, 0x63, 0x68, 0x65, 0x72, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x09,
|
||||
0x52, 0x0c, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x52, 0x75, 0x6c, 0x65, 0x73, 0x12, 0x20,
|
||||
0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20,
|
||||
0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x22, 0x1f, 0x0a, 0x0d, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x75, 0x63, 0x68, 0x65,
|
||||
0x72, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x02, 0x69,
|
||||
0x64, 0x22, 0xa7, 0x01, 0x0a, 0x19, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x53, 0x75, 0x62, 0x73,
|
||||
0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12,
|
||||
0x13, 0x0a, 0x02, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x02, 0x69,
|
||||
0x64, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x0c, 0x6f, 0x66, 0x66, 0x65, 0x72, 0x69, 0x6e, 0x67,
|
||||
0x43, 0x6f, 0x64, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x6f, 0x66, 0x66, 0x65,
|
||||
0x72, 0x69, 0x6e, 0x67, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x20, 0x0a, 0x0b, 0x73, 0x69, 0x67, 0x6e,
|
||||
0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x73,
|
||||
0x69, 0x67, 0x6e, 0x69, 0x6e, 0x67, 0x54, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, 0x04, 0x64, 0x61,
|
||||
0x74, 0x61, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
|
||||
0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41, 0x6e, 0x79, 0x52, 0x04,
|
||||
0x64, 0x61, 0x74, 0x61, 0x42, 0x05, 0x0a, 0x03, 0x5f, 0x69, 0x64, 0x22, 0x3a, 0x0a, 0x0b, 0x53,
|
||||
0x65, 0x74, 0x43, 0x61, 0x72, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x2b, 0x0a, 0x04, 0x74, 0x79,
|
||||
0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f,
|
||||
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x43, 0x61, 0x72, 0x74, 0x54, 0x79, 0x70,
|
||||
0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x22, 0xea, 0x07, 0x0a, 0x08, 0x4d, 0x75, 0x74, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x12, 0x40, 0x0a, 0x0a, 0x63, 0x6c, 0x65, 0x61, 0x72, 0x5f, 0x63, 0x61,
|
||||
0x72, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f,
|
||||
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x43, 0x6c, 0x65, 0x61, 0x72, 0x43, 0x61,
|
||||
0x72, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x48, 0x00, 0x52, 0x09, 0x63, 0x6c, 0x65,
|
||||
0x61, 0x72, 0x43, 0x61, 0x72, 0x74, 0x12, 0x33, 0x0a, 0x08, 0x61, 0x64, 0x64, 0x5f, 0x69, 0x74,
|
||||
0x65, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f,
|
||||
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x41, 0x64, 0x64, 0x49, 0x74, 0x65, 0x6d,
|
||||
0x48, 0x00, 0x52, 0x07, 0x61, 0x64, 0x64, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x3c, 0x0a, 0x0b, 0x72,
|
||||
0x65, 0x6d, 0x6f, 0x76, 0x65, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b,
|
||||
0x32, 0x19, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73,
|
||||
0x2e, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x48, 0x00, 0x52, 0x0a, 0x72,
|
||||
0x65, 0x6d, 0x6f, 0x76, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x12, 0x48, 0x0a, 0x0f, 0x63, 0x68, 0x61,
|
||||
0x6e, 0x67, 0x65, 0x5f, 0x71, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x04, 0x20, 0x01,
|
||||
0x28, 0x0b, 0x32, 0x1d, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67,
|
||||
0x65, 0x73, 0x2e, 0x43, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x51, 0x75, 0x61, 0x6e, 0x74, 0x69, 0x74,
|
||||
0x79, 0x48, 0x00, 0x52, 0x0e, 0x63, 0x68, 0x61, 0x6e, 0x67, 0x65, 0x51, 0x75, 0x61, 0x6e, 0x74,
|
||||
0x69, 0x74, 0x79, 0x12, 0x3a, 0x0a, 0x0b, 0x73, 0x65, 0x74, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f,
|
||||
0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x53, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72,
|
||||
0x49, 0x64, 0x48, 0x00, 0x52, 0x09, 0x73, 0x65, 0x74, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12,
|
||||
0x4c, 0x0a, 0x11, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x74, 0x65, 0x6d, 0x5f, 0x6d, 0x61, 0x72,
|
||||
0x6b, 0x69, 0x6e, 0x67, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1e, 0x2e, 0x63, 0x61, 0x72,
|
||||
0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x4c, 0x69, 0x6e, 0x65, 0x49,
|
||||
0x74, 0x65, 0x6d, 0x4d, 0x61, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x0f, 0x6c, 0x69,
|
||||
0x6e, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x4d, 0x61, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x12, 0x5f, 0x0a,
|
||||
0x18, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x74, 0x65,
|
||||
0x6d, 0x5f, 0x6d, 0x61, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x24, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e,
|
||||
0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4c, 0x69, 0x6e, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x4d, 0x61,
|
||||
0x72, 0x6b, 0x69, 0x6e, 0x67, 0x48, 0x00, 0x52, 0x15, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x4c,
|
||||
0x69, 0x6e, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x4d, 0x61, 0x72, 0x6b, 0x69, 0x6e, 0x67, 0x12, 0x51,
|
||||
0x0a, 0x12, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61,
|
||||
0x64, 0x64, 0x65, 0x64, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x61, 0x72,
|
||||
0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x53, 0x75, 0x62, 0x73, 0x63,
|
||||
0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x64, 0x64, 0x65, 0x64, 0x48, 0x00, 0x52, 0x11,
|
||||
0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x64, 0x64, 0x65,
|
||||
0x64, 0x12, 0x66, 0x0a, 0x1b, 0x73, 0x65, 0x74, 0x5f, 0x6c, 0x69, 0x6e, 0x65, 0x5f, 0x69, 0x74,
|
||||
0x65, 0x6d, 0x5f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x73,
|
||||
0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x65,
|
||||
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x53, 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x65, 0x49, 0x74,
|
||||
0x65, 0x6d, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x48, 0x00,
|
||||
0x52, 0x17, 0x73, 0x65, 0x74, 0x4c, 0x69, 0x6e, 0x65, 0x49, 0x74, 0x65, 0x6d, 0x43, 0x75, 0x73,
|
||||
0x74, 0x6f, 0x6d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x73, 0x12, 0x3c, 0x0a, 0x0b, 0x61, 0x64, 0x64,
|
||||
0x5f, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x18, 0x14, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19,
|
||||
0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x41,
|
||||
0x64, 0x64, 0x56, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x48, 0x00, 0x52, 0x0a, 0x61, 0x64, 0x64,
|
||||
0x56, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x12, 0x45, 0x0a, 0x0e, 0x72, 0x65, 0x6d, 0x6f, 0x76,
|
||||
0x65, 0x5f, 0x76, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x18, 0x15, 0x20, 0x01, 0x28, 0x0b, 0x32,
|
||||
0x1c, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e,
|
||||
0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x48, 0x00, 0x52,
|
||||
0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x56, 0x6f, 0x75, 0x63, 0x68, 0x65, 0x72, 0x12, 0x6a,
|
||||
0x0a, 0x1b, 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x5f, 0x73, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69,
|
||||
0x70, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x64, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x18, 0x16, 0x20,
|
||||
0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61,
|
||||
0x67, 0x65, 0x73, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72,
|
||||
0x69, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x48, 0x00, 0x52,
|
||||
0x19, 0x75, 0x70, 0x73, 0x65, 0x72, 0x74, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x44, 0x65, 0x74, 0x61, 0x69, 0x6c, 0x73, 0x12, 0x40, 0x0a, 0x0d, 0x73, 0x65,
|
||||
0x74, 0x5f, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x17, 0x20, 0x01, 0x28,
|
||||
0x0b, 0x32, 0x1a, 0x2e, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||
0x73, 0x2e, 0x53, 0x65, 0x74, 0x43, 0x61, 0x72, 0x74, 0x54, 0x79, 0x70, 0x65, 0x48, 0x00, 0x52,
|
||||
0x0b, 0x73, 0x65, 0x74, 0x43, 0x61, 0x72, 0x74, 0x54, 0x79, 0x70, 0x65, 0x42, 0x06, 0x0a, 0x04,
|
||||
0x74, 0x79, 0x70, 0x65, 0x2a, 0x30, 0x0a, 0x08, 0x43, 0x61, 0x72, 0x74, 0x54, 0x79, 0x70, 0x65,
|
||||
0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x47, 0x55, 0x4c, 0x41, 0x52, 0x10, 0x00, 0x12, 0x0c, 0x0a,
|
||||
0x08, 0x57, 0x49, 0x53, 0x48, 0x4c, 0x49, 0x53, 0x54, 0x10, 0x01, 0x12, 0x09, 0x0a, 0x05, 0x4f,
|
||||
0x46, 0x46, 0x45, 0x52, 0x10, 0x02, 0x42, 0x39, 0x5a, 0x37, 0x67, 0x69, 0x74, 0x2e, 0x6b, 0x36,
|
||||
0x6e, 0x2e, 0x6e, 0x65, 0x74, 0x2f, 0x6d, 0x61, 0x74, 0x73, 0x2f, 0x67, 0x6f, 0x2d, 0x63, 0x61,
|
||||
0x72, 0x74, 0x2d, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x63,
|
||||
0x61, 0x72, 0x74, 0x3b, 0x63, 0x61, 0x72, 0x74, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65,
|
||||
0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
})
|
||||
const file_cart_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\n" +
|
||||
"cart.proto\x12\rcart_messages\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\x12\n" +
|
||||
"\x10ClearCartRequest\"\xaa\b\n" +
|
||||
"\aAddItem\x12\x17\n" +
|
||||
"\aitem_id\x18\x01 \x01(\rR\x06itemId\x12\x1a\n" +
|
||||
"\bquantity\x18\x02 \x01(\x05R\bquantity\x12\x14\n" +
|
||||
"\x05price\x18\x03 \x01(\x03R\x05price\x12\x1a\n" +
|
||||
"\borgPrice\x18\t \x01(\x03R\borgPrice\x12\x10\n" +
|
||||
"\x03sku\x18\x04 \x01(\tR\x03sku\x12\x12\n" +
|
||||
"\x04name\x18\x05 \x01(\tR\x04name\x12\x14\n" +
|
||||
"\x05image\x18\x06 \x01(\tR\x05image\x12\x14\n" +
|
||||
"\x05stock\x18\a \x01(\x05R\x05stock\x12\x10\n" +
|
||||
"\x03tax\x18\b \x01(\x05R\x03tax\x12\x14\n" +
|
||||
"\x05brand\x18\r \x01(\tR\x05brand\x12\x1a\n" +
|
||||
"\bcategory\x18\x0e \x01(\tR\bcategory\x12\x1c\n" +
|
||||
"\tcategory2\x18\x0f \x01(\tR\tcategory2\x12\x1c\n" +
|
||||
"\tcategory3\x18\x10 \x01(\tR\tcategory3\x12\x1c\n" +
|
||||
"\tcategory4\x18\x11 \x01(\tR\tcategory4\x12\x1c\n" +
|
||||
"\tcategory5\x18\x12 \x01(\tR\tcategory5\x12\x1e\n" +
|
||||
"\n" +
|
||||
"disclaimer\x18\n" +
|
||||
" \x01(\tR\n" +
|
||||
"disclaimer\x12 \n" +
|
||||
"\varticleType\x18\v \x01(\tR\varticleType\x12\x1a\n" +
|
||||
"\bsellerId\x18\x13 \x01(\tR\bsellerId\x12\x1e\n" +
|
||||
"\n" +
|
||||
"sellerName\x18\x14 \x01(\tR\n" +
|
||||
"sellerName\x12\x18\n" +
|
||||
"\acountry\x18\x15 \x01(\tR\acountry\x12\x1e\n" +
|
||||
"\n" +
|
||||
"saleStatus\x18\x18 \x01(\tR\n" +
|
||||
"saleStatus\x12\x1b\n" +
|
||||
"\x06outlet\x18\f \x01(\tH\x00R\x06outlet\x88\x01\x01\x12\x1d\n" +
|
||||
"\astoreId\x18\x16 \x01(\tH\x01R\astoreId\x88\x01\x01\x12\x1f\n" +
|
||||
"\bparentId\x18\x17 \x01(\rH\x02R\bparentId\x88\x01\x01\x12\x10\n" +
|
||||
"\x03cgm\x18\x19 \x01(\tR\x03cgm\x12O\n" +
|
||||
"\x12reservationEndTime\x18\x1a \x01(\v2\x1a.google.protobuf.TimestampH\x03R\x12reservationEndTime\x88\x01\x01\x12\x1d\n" +
|
||||
"\n" +
|
||||
"extra_json\x18\x1b \x01(\fR\textraJson\x12M\n" +
|
||||
"\rcustom_fields\x18\x1c \x03(\v2(.cart_messages.AddItem.CustomFieldsEntryR\fcustomFields\x12+\n" +
|
||||
"\x11inventory_tracked\x18\x1d \x01(\bR\x10inventoryTracked\x12\x1b\n" +
|
||||
"\tdrop_ship\x18\x1e \x01(\bR\bdropShip\x1a?\n" +
|
||||
"\x11CustomFieldsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01B\t\n" +
|
||||
"\a_outletB\n" +
|
||||
"\n" +
|
||||
"\b_storeIdB\v\n" +
|
||||
"\t_parentIdB\x15\n" +
|
||||
"\x13_reservationEndTime\"\x1c\n" +
|
||||
"\n" +
|
||||
"RemoveItem\x12\x0e\n" +
|
||||
"\x02Id\x18\x01 \x01(\rR\x02Id\"<\n" +
|
||||
"\x0eChangeQuantity\x12\x0e\n" +
|
||||
"\x02Id\x18\x01 \x01(\rR\x02Id\x12\x1a\n" +
|
||||
"\bquantity\x18\x02 \x01(\x05R\bquantity\"#\n" +
|
||||
"\tSetUserId\x12\x16\n" +
|
||||
"\x06userId\x18\x01 \x01(\tR\x06userId\"O\n" +
|
||||
"\x0fLineItemMarking\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\rR\x02id\x12\x12\n" +
|
||||
"\x04type\x18\x02 \x01(\rR\x04type\x12\x18\n" +
|
||||
"\amarking\x18\x03 \x01(\tR\amarking\"'\n" +
|
||||
"\x15RemoveLineItemMarking\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\rR\x02id\"\xc9\x01\n" +
|
||||
"\x17SetLineItemCustomFields\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\rR\x02id\x12]\n" +
|
||||
"\rcustom_fields\x18\x02 \x03(\v28.cart_messages.SetLineItemCustomFields.CustomFieldsEntryR\fcustomFields\x1a?\n" +
|
||||
"\x11CustomFieldsEntry\x12\x10\n" +
|
||||
"\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"q\n" +
|
||||
"\x11SubscriptionAdded\x12\x16\n" +
|
||||
"\x06itemId\x18\x01 \x01(\rR\x06itemId\x12\x1c\n" +
|
||||
"\tdetailsId\x18\x03 \x01(\tR\tdetailsId\x12&\n" +
|
||||
"\x0eorderReference\x18\x04 \x01(\tR\x0eorderReference\"|\n" +
|
||||
"\n" +
|
||||
"AddVoucher\x12\x12\n" +
|
||||
"\x04code\x18\x01 \x01(\tR\x04code\x12\x14\n" +
|
||||
"\x05value\x18\x02 \x01(\x03R\x05value\x12\"\n" +
|
||||
"\fvoucherRules\x18\x03 \x03(\tR\fvoucherRules\x12 \n" +
|
||||
"\vdescription\x18\x04 \x01(\tR\vdescription\"\x1f\n" +
|
||||
"\rRemoveVoucher\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\rR\x02id\"\xa7\x01\n" +
|
||||
"\x19UpsertSubscriptionDetails\x12\x13\n" +
|
||||
"\x02id\x18\x01 \x01(\tH\x00R\x02id\x88\x01\x01\x12\"\n" +
|
||||
"\fofferingCode\x18\x02 \x01(\tR\fofferingCode\x12 \n" +
|
||||
"\vsigningType\x18\x03 \x01(\tR\vsigningType\x12(\n" +
|
||||
"\x04data\x18\x04 \x01(\v2\x14.google.protobuf.AnyR\x04dataB\x05\n" +
|
||||
"\x03_id\":\n" +
|
||||
"\vSetCartType\x12+\n" +
|
||||
"\x04type\x18\x01 \x01(\x0e2\x17.cart_messages.CartTypeR\x04type\"=\n" +
|
||||
"\tPushToken\x12\x1a\n" +
|
||||
"\bplatform\x18\x01 \x01(\tR\bplatform\x12\x14\n" +
|
||||
"\x05token\x18\x02 \x01(\tR\x05token\"e\n" +
|
||||
"\x12SetRecoveryContact\x12\x14\n" +
|
||||
"\x05email\x18\x01 \x01(\tR\x05email\x129\n" +
|
||||
"\vpush_tokens\x18\x02 \x03(\v2\x18.cart_messages.PushTokenR\n" +
|
||||
"pushTokens\"\xc1\b\n" +
|
||||
"\bMutation\x12@\n" +
|
||||
"\n" +
|
||||
"clear_cart\x18\x01 \x01(\v2\x1f.cart_messages.ClearCartRequestH\x00R\tclearCart\x123\n" +
|
||||
"\badd_item\x18\x02 \x01(\v2\x16.cart_messages.AddItemH\x00R\aaddItem\x12<\n" +
|
||||
"\vremove_item\x18\x03 \x01(\v2\x19.cart_messages.RemoveItemH\x00R\n" +
|
||||
"removeItem\x12H\n" +
|
||||
"\x0fchange_quantity\x18\x04 \x01(\v2\x1d.cart_messages.ChangeQuantityH\x00R\x0echangeQuantity\x12:\n" +
|
||||
"\vset_user_id\x18\x05 \x01(\v2\x18.cart_messages.SetUserIdH\x00R\tsetUserId\x12L\n" +
|
||||
"\x11line_item_marking\x18\x06 \x01(\v2\x1e.cart_messages.LineItemMarkingH\x00R\x0flineItemMarking\x12_\n" +
|
||||
"\x18remove_line_item_marking\x18\a \x01(\v2$.cart_messages.RemoveLineItemMarkingH\x00R\x15removeLineItemMarking\x12Q\n" +
|
||||
"\x12subscription_added\x18\b \x01(\v2 .cart_messages.SubscriptionAddedH\x00R\x11subscriptionAdded\x12f\n" +
|
||||
"\x1bset_line_item_custom_fields\x18\t \x01(\v2&.cart_messages.SetLineItemCustomFieldsH\x00R\x17setLineItemCustomFields\x12<\n" +
|
||||
"\vadd_voucher\x18\x14 \x01(\v2\x19.cart_messages.AddVoucherH\x00R\n" +
|
||||
"addVoucher\x12E\n" +
|
||||
"\x0eremove_voucher\x18\x15 \x01(\v2\x1c.cart_messages.RemoveVoucherH\x00R\rremoveVoucher\x12j\n" +
|
||||
"\x1bupsert_subscription_details\x18\x16 \x01(\v2(.cart_messages.UpsertSubscriptionDetailsH\x00R\x19upsertSubscriptionDetails\x12@\n" +
|
||||
"\rset_cart_type\x18\x17 \x01(\v2\x1a.cart_messages.SetCartTypeH\x00R\vsetCartType\x12U\n" +
|
||||
"\x14set_recovery_contact\x18\x18 \x01(\v2!.cart_messages.SetRecoveryContactH\x00R\x12setRecoveryContactB\x06\n" +
|
||||
"\x04type*0\n" +
|
||||
"\bCartType\x12\v\n" +
|
||||
"\aREGULAR\x10\x00\x12\f\n" +
|
||||
"\bWISHLIST\x10\x01\x12\t\n" +
|
||||
"\x05OFFER\x10\x02B9Z7git.k6n.net/mats/go-cart-actor/proto/cart;cart_messagesb\x06proto3"
|
||||
|
||||
var (
|
||||
file_cart_proto_rawDescOnce sync.Once
|
||||
@@ -1451,7 +1496,7 @@ func file_cart_proto_rawDescGZIP() []byte {
|
||||
}
|
||||
|
||||
var file_cart_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
|
||||
var file_cart_proto_msgTypes = make([]protoimpl.MessageInfo, 16)
|
||||
var file_cart_proto_msgTypes = make([]protoimpl.MessageInfo, 18)
|
||||
var file_cart_proto_goTypes = []any{
|
||||
(CartType)(0), // 0: cart_messages.CartType
|
||||
(*ClearCartRequest)(nil), // 1: cart_messages.ClearCartRequest
|
||||
@@ -1467,36 +1512,40 @@ var file_cart_proto_goTypes = []any{
|
||||
(*RemoveVoucher)(nil), // 11: cart_messages.RemoveVoucher
|
||||
(*UpsertSubscriptionDetails)(nil), // 12: cart_messages.UpsertSubscriptionDetails
|
||||
(*SetCartType)(nil), // 13: cart_messages.SetCartType
|
||||
(*Mutation)(nil), // 14: cart_messages.Mutation
|
||||
nil, // 15: cart_messages.AddItem.CustomFieldsEntry
|
||||
nil, // 16: cart_messages.SetLineItemCustomFields.CustomFieldsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 17: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 18: google.protobuf.Any
|
||||
(*PushToken)(nil), // 14: cart_messages.PushToken
|
||||
(*SetRecoveryContact)(nil), // 15: cart_messages.SetRecoveryContact
|
||||
(*Mutation)(nil), // 16: cart_messages.Mutation
|
||||
nil, // 17: cart_messages.AddItem.CustomFieldsEntry
|
||||
nil, // 18: cart_messages.SetLineItemCustomFields.CustomFieldsEntry
|
||||
(*timestamppb.Timestamp)(nil), // 19: google.protobuf.Timestamp
|
||||
(*anypb.Any)(nil), // 20: google.protobuf.Any
|
||||
}
|
||||
var file_cart_proto_depIdxs = []int32{
|
||||
17, // 0: cart_messages.AddItem.reservationEndTime:type_name -> google.protobuf.Timestamp
|
||||
15, // 1: cart_messages.AddItem.custom_fields:type_name -> cart_messages.AddItem.CustomFieldsEntry
|
||||
16, // 2: cart_messages.SetLineItemCustomFields.custom_fields:type_name -> cart_messages.SetLineItemCustomFields.CustomFieldsEntry
|
||||
18, // 3: cart_messages.UpsertSubscriptionDetails.data:type_name -> google.protobuf.Any
|
||||
19, // 0: cart_messages.AddItem.reservationEndTime:type_name -> google.protobuf.Timestamp
|
||||
17, // 1: cart_messages.AddItem.custom_fields:type_name -> cart_messages.AddItem.CustomFieldsEntry
|
||||
18, // 2: cart_messages.SetLineItemCustomFields.custom_fields:type_name -> cart_messages.SetLineItemCustomFields.CustomFieldsEntry
|
||||
20, // 3: cart_messages.UpsertSubscriptionDetails.data:type_name -> google.protobuf.Any
|
||||
0, // 4: cart_messages.SetCartType.type:type_name -> cart_messages.CartType
|
||||
1, // 5: cart_messages.Mutation.clear_cart:type_name -> cart_messages.ClearCartRequest
|
||||
2, // 6: cart_messages.Mutation.add_item:type_name -> cart_messages.AddItem
|
||||
3, // 7: cart_messages.Mutation.remove_item:type_name -> cart_messages.RemoveItem
|
||||
4, // 8: cart_messages.Mutation.change_quantity:type_name -> cart_messages.ChangeQuantity
|
||||
5, // 9: cart_messages.Mutation.set_user_id:type_name -> cart_messages.SetUserId
|
||||
6, // 10: cart_messages.Mutation.line_item_marking:type_name -> cart_messages.LineItemMarking
|
||||
7, // 11: cart_messages.Mutation.remove_line_item_marking:type_name -> cart_messages.RemoveLineItemMarking
|
||||
9, // 12: cart_messages.Mutation.subscription_added:type_name -> cart_messages.SubscriptionAdded
|
||||
8, // 13: cart_messages.Mutation.set_line_item_custom_fields:type_name -> cart_messages.SetLineItemCustomFields
|
||||
10, // 14: cart_messages.Mutation.add_voucher:type_name -> cart_messages.AddVoucher
|
||||
11, // 15: cart_messages.Mutation.remove_voucher:type_name -> cart_messages.RemoveVoucher
|
||||
12, // 16: cart_messages.Mutation.upsert_subscription_details:type_name -> cart_messages.UpsertSubscriptionDetails
|
||||
13, // 17: cart_messages.Mutation.set_cart_type:type_name -> cart_messages.SetCartType
|
||||
18, // [18:18] is the sub-list for method output_type
|
||||
18, // [18:18] is the sub-list for method input_type
|
||||
18, // [18:18] is the sub-list for extension type_name
|
||||
18, // [18:18] is the sub-list for extension extendee
|
||||
0, // [0:18] is the sub-list for field type_name
|
||||
14, // 5: cart_messages.SetRecoveryContact.push_tokens:type_name -> cart_messages.PushToken
|
||||
1, // 6: cart_messages.Mutation.clear_cart:type_name -> cart_messages.ClearCartRequest
|
||||
2, // 7: cart_messages.Mutation.add_item:type_name -> cart_messages.AddItem
|
||||
3, // 8: cart_messages.Mutation.remove_item:type_name -> cart_messages.RemoveItem
|
||||
4, // 9: cart_messages.Mutation.change_quantity:type_name -> cart_messages.ChangeQuantity
|
||||
5, // 10: cart_messages.Mutation.set_user_id:type_name -> cart_messages.SetUserId
|
||||
6, // 11: cart_messages.Mutation.line_item_marking:type_name -> cart_messages.LineItemMarking
|
||||
7, // 12: cart_messages.Mutation.remove_line_item_marking:type_name -> cart_messages.RemoveLineItemMarking
|
||||
9, // 13: cart_messages.Mutation.subscription_added:type_name -> cart_messages.SubscriptionAdded
|
||||
8, // 14: cart_messages.Mutation.set_line_item_custom_fields:type_name -> cart_messages.SetLineItemCustomFields
|
||||
10, // 15: cart_messages.Mutation.add_voucher:type_name -> cart_messages.AddVoucher
|
||||
11, // 16: cart_messages.Mutation.remove_voucher:type_name -> cart_messages.RemoveVoucher
|
||||
12, // 17: cart_messages.Mutation.upsert_subscription_details:type_name -> cart_messages.UpsertSubscriptionDetails
|
||||
13, // 18: cart_messages.Mutation.set_cart_type:type_name -> cart_messages.SetCartType
|
||||
15, // 19: cart_messages.Mutation.set_recovery_contact:type_name -> cart_messages.SetRecoveryContact
|
||||
20, // [20:20] is the sub-list for method output_type
|
||||
20, // [20:20] is the sub-list for method input_type
|
||||
20, // [20:20] is the sub-list for extension type_name
|
||||
20, // [20:20] is the sub-list for extension extendee
|
||||
0, // [0:20] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_cart_proto_init() }
|
||||
@@ -1506,7 +1555,7 @@ func file_cart_proto_init() {
|
||||
}
|
||||
file_cart_proto_msgTypes[1].OneofWrappers = []any{}
|
||||
file_cart_proto_msgTypes[11].OneofWrappers = []any{}
|
||||
file_cart_proto_msgTypes[13].OneofWrappers = []any{
|
||||
file_cart_proto_msgTypes[15].OneofWrappers = []any{
|
||||
(*Mutation_ClearCart)(nil),
|
||||
(*Mutation_AddItem)(nil),
|
||||
(*Mutation_RemoveItem)(nil),
|
||||
@@ -1520,6 +1569,7 @@ func file_cart_proto_init() {
|
||||
(*Mutation_RemoveVoucher)(nil),
|
||||
(*Mutation_UpsertSubscriptionDetails)(nil),
|
||||
(*Mutation_SetCartType)(nil),
|
||||
(*Mutation_SetRecoveryContact)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
@@ -1527,7 +1577,7 @@ func file_cart_proto_init() {
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_cart_proto_rawDesc), len(file_cart_proto_rawDesc)),
|
||||
NumEnums: 1,
|
||||
NumMessages: 16,
|
||||
NumMessages: 18,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user