From be9ecd538bb51139ac2795471e39af0229de0c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mats=20T=C3=B6rnberg?= Date: Mon, 24 Aug 2026 12:49:30 +0200 Subject: [PATCH] abandoned-cart visibility: capture-mode campaigns + admin view Bumps go-cart-actor (profile-link recovery-contact backfill, capture email sender, /carts/abandoned read API) and backoffice (Abandoned carts panel, storefront recovery-contact wiring). Compose shares CAMPAIGN_DIR between cart service and backoffice and sets CART_EMAIL_SENDER=capture for dev. Plan doc: docs/plan-abandoned-carts-visibility.md. --- event/event.go | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/event/event.go b/event/event.go index 3e28966..da4e6a2 100644 --- a/event/event.go +++ b/event/event.go @@ -108,6 +108,20 @@ const ( // 5 hand-written `pevent.Type("track.purchase")` literals with this // typed constant. TrackPurchase Type = "track.purchase" + // ServerPurchase — the server-side confirmed-order fallback for + // track.purchase (wishlist dual-channel style, W6d pattern). Emitted by + // the order service's durable outbox when an order reaches StatusCaptured + // (the same atomic emitOnApply that publishes order.created), so a + // purchase is recorded even when the browser beacon was blocked (no JS / + // no consent). Deliberately session-less: it carries catalog item ids + // only — no session id, no customer identity (C5) — and feeds the + // session-less co-occurrence AlsoBought intra-pair on the tracking side. + // It must NOT ride the browser `track.purchase` type: the consumer + // routes that through the session-keyed HandlePurchaseEvent (funnel + + // converted label), which a session-less event would corrupt. Wire + // routing key `track.purchase_server` rides the existing `track.*` + // binding on the catalog exchange. Payload is an event.ServerPurchaseEvent. + ServerPurchase Type = "track.purchase_server" // TrackPromotionView / TrackPromotionClick — storefront promotion // attribution. Producer: the storefront promotion-badge islands, PLP // promo badges, bundle picker and cart bundle upsell (lib/track.ts @@ -215,6 +229,32 @@ type WishlistMutationEvent struct { ItemId uint32 `json:"itemId,omitempty"` } +// PurchaseItem is one line of a confirmed order in a ServerPurchaseEvent. +// ItemId is the numeric catalog (finder) item id the tracking pipeline keys +// on; lines without one (added without catalog facts) are skipped by the +// producer. Quantity mirrors the order line quantity. +type PurchaseItem struct { + Sku string `json:"sku,omitempty"` + ItemId uint32 `json:"itemId"` + Quantity uint `json:"quantity,omitempty"` +} + +// ServerPurchaseEvent is the payload for track.purchase_server (the order +// service's confirmed-order fallback). Deliberately pseudonymous (C5): order +// id + catalog item ids + optional session id only — never the customer +// email/name/profile id, so the tracking pipeline can pair "these items were +// bought together" without being able to attribute the purchase to a person. +// SessionId is the tracking session (sid) captured at checkout start and +// threaded through the checkout→order handoff; when > 0 the tracking +// consumer routes the purchase through the session-keyed path (funnel + +// converted label), when 0 it stays session-less (no consent / server-side +// checkout). +type ServerPurchaseEvent struct { + OrderID string `json:"orderId"` + SessionId int64 `json:"session_id,omitempty"` + Items []PurchaseItem `json:"items"` +} + // CartToWishlistEvent is the payload for cart.to_wishlist (cart service). // Carries the pseudonymised owner profile id (the cart's UserId — a // customer-scoped opaque uint64 id, never an email/name) plus the list the