Factoid: hardcoded "Grocery". Receipt: Retailer ServiceGetRetailerByStoreName() normalized to one of: Grocery, Drugstore, Warehouse, Convenience, Big Box; defaults to "Grocery"
Filters: 2+ purchases, not Alcohol, last purchase within lookback window (default 90 days), has a computed avg interval.
Returned fields consumed by scheduler: timestamps array is used for time-of-day analysis to determine optimal notification delivery time. timezone is used to convert to local time.
The transformer creates CategoryData with the normalized ID, but linkProductsToCategories in the writer re-derives the category ID from the raw Product.Category name using the writer’s non-normalizing version. This means the IN_CATEGORY MATCH may fail to find the category node that was created with the normalized ID.
The timestamps and receipt_ids arrays on PURCHASED relationships grow without bound — every new purchase appends. For frequently-purchased products this can become large over time.
Community.member_count increments on every user assignment but never decrements if a user’s category preferences shift. The count drifts upward over time.
Indexes are created at worker boot via Repository.EnsureIndexes in
internal/graph/writer.go (idempotent via IF NOT EXISTS):
Index
Type
On
user_id_idx
RANGE
(:User).user_id
product_id_idx
RANGE
(:Product).product_id
category_id_idx
RANGE
(:Category).category_id
retailer_id_idx
RANGE
(:Retailer).retailer_id
community_id_idx
RANGE
(:Community).community_id
purchased_last_idx
RANGE
()-[:PURCHASED]-().last — added so the scheduler’s repurchase-candidate query can range-seek the time window instead of post-filter scanning. See internal/notification/repurchase/neo4j.go GetRepurchaseCandidatesBatch.
No CREATE CONSTRAINT statements exist in the codebase.