max / makenotwork
1 file changed,
+33 insertions,
-19 deletions
| @@ -10,6 +10,14 @@ use std::sync::atomic::{AtomicU32, Ordering}; | |||
| 10 | 10 | /// Monotonic counter for unique buyer usernames across all tests. | |
| 11 | 11 | static BUYER_COUNTER: AtomicU32 = AtomicU32::new(0); | |
| 12 | 12 | ||
| 13 | + | /// Returns an ISO 8601 timestamp for `days` days ago at the given `hour` (UTC). | |
| 14 | + | /// Uses date-only arithmetic so two calls with the same `days` but different | |
| 15 | + | /// `hour` values always land on the same calendar day. | |
| 16 | + | fn days_ago_at(days: u32, hour: u32) -> String { | |
| 17 | + | let date = (chrono::Utc::now() - chrono::Duration::days(days as i64)).date_naive(); | |
| 18 | + | format!("{}T{:02}:00:00Z", date, hour) | |
| 19 | + | } | |
| 20 | + | ||
| 13 | 21 | /// Create a unique buyer user via direct SQL. Avoids the partial unique index | |
| 14 | 22 | /// on transactions(buyer_id, item_id) WHERE status = 'completed' by giving | |
| 15 | 23 | /// each transaction its own buyer. | |
| @@ -108,10 +116,13 @@ async fn revenue_timeseries_buckets_by_day() { | |||
| 108 | 116 | let mut h = TestHarness::new().await; | |
| 109 | 117 | let (seller_id, _, item_id) = setup_seller_with_item(&mut h, "ts1").await; | |
| 110 | 118 | ||
| 111 | - | // Insert 3 transactions across 2 days | |
| 112 | - | insert_transaction(&h.db, seller_id, item_id, 1000, "2026-03-03T12:00:00Z").await; | |
| 113 | - | insert_transaction(&h.db, seller_id, item_id, 2000, "2026-03-03T14:00:00Z").await; | |
| 114 | - | insert_transaction(&h.db, seller_id, item_id, 500, "2026-03-02T10:00:00Z").await; | |
| 119 | + | // Insert 3 transactions across 2 days (all within 30-day window) | |
| 120 | + | let day_a = days_ago_at(2, 12); | |
| 121 | + | let day_a2 = days_ago_at(2, 14); | |
| 122 | + | let day_b = days_ago_at(3, 10); | |
| 123 | + | insert_transaction(&h.db, seller_id, item_id, 1000, &day_a).await; | |
| 124 | + | insert_transaction(&h.db, seller_id, item_id, 2000, &day_a2).await; | |
| 125 | + | insert_transaction(&h.db, seller_id, item_id, 500, &day_b).await; | |
| 115 | 126 | ||
| 116 | 127 | // Timeseries bucketed by day, user scope, 30d range | |
| 117 | 128 | let rows: Vec<(chrono::DateTime<chrono::Utc>, i64, i64)> = sqlx::query_as( | |
| @@ -136,11 +147,11 @@ async fn revenue_timeseries_buckets_by_day() { | |||
| 136 | 147 | ||
| 137 | 148 | assert_eq!(rows.len(), 2, "Expected 2 daily buckets, got {}", rows.len()); | |
| 138 | 149 | ||
| 139 | - | // Mar 2: 1 sale, 500 cents | |
| 150 | + | // Earlier day: 1 sale, 500 cents | |
| 140 | 151 | assert_eq!(rows[0].1, 500); | |
| 141 | 152 | assert_eq!(rows[0].2, 1); | |
| 142 | 153 | ||
| 143 | - | // Mar 3: 2 sales, 3000 cents | |
| 154 | + | // Later day: 2 sales, 3000 cents | |
| 144 | 155 | assert_eq!(rows[1].1, 3000); | |
| 145 | 156 | assert_eq!(rows[1].2, 2); | |
| 146 | 157 | } | |
| @@ -177,10 +188,13 @@ async fn revenue_timeseries_item_and_project_scope() { | |||
| 177 | 188 | .await | |
| 178 | 189 | .unwrap(); | |
| 179 | 190 | ||
| 180 | - | // Transactions: item_a on Mar 2 and Mar 3, item_b on Mar 3 only | |
| 181 | - | insert_transaction(&h.db, seller_id, item_a, 1000, "2026-03-02T12:00:00Z").await; | |
| 182 | - | insert_transaction(&h.db, seller_id, item_a, 1000, "2026-03-03T12:00:00Z").await; | |
| 183 | - | insert_transaction(&h.db, seller_id, item_b, 2000, "2026-03-03T13:00:00Z").await; | |
| 191 | + | // Transactions: item_a on day_b and day_a, item_b on day_a only | |
| 192 | + | let day_a = days_ago_at(2, 12); | |
| 193 | + | let day_a2 = days_ago_at(2, 13); | |
| 194 | + | let day_b = days_ago_at(3, 12); | |
| 195 | + | insert_transaction(&h.db, seller_id, item_a, 1000, &day_b).await; | |
| 196 | + | insert_transaction(&h.db, seller_id, item_a, 1000, &day_a).await; | |
| 197 | + | insert_transaction(&h.db, seller_id, item_b, 2000, &day_a2).await; | |
| 184 | 198 | ||
| 185 | 199 | // Item scope: only item_a | |
| 186 | 200 | let rows: Vec<(chrono::DateTime<chrono::Utc>, i64, i64)> = sqlx::query_as( | |
| @@ -233,9 +247,9 @@ async fn revenue_timeseries_item_and_project_scope() { | |||
| 233 | 247 | .unwrap(); | |
| 234 | 248 | ||
| 235 | 249 | assert_eq!(rows.len(), 2, "Project scope should have 2 daily buckets"); | |
| 236 | - | assert_eq!(rows[0].1, 1000, "Mar 2: only item_a"); | |
| 237 | - | assert_eq!(rows[1].1, 3000, "Mar 3: item_a + item_b"); | |
| 238 | - | assert_eq!(rows[1].2, 2, "Mar 3: 2 sales total"); | |
| 250 | + | assert_eq!(rows[0].1, 1000, "Earlier day: only item_a"); | |
| 251 | + | assert_eq!(rows[1].1, 3000, "Later day: item_a + item_b"); | |
| 252 | + | assert_eq!(rows[1].2, 2, "Later day: 2 sales total"); | |
| 239 | 253 | } | |
| 240 | 254 | ||
| 241 | 255 | #[tokio::test] | |
| @@ -244,11 +258,11 @@ async fn period_comparison_current_vs_previous() { | |||
| 244 | 258 | let (seller_id, _, item_id) = setup_seller_with_item(&mut h, "cmp1").await; | |
| 245 | 259 | ||
| 246 | 260 | // Current period (within last 7 days): 2 sales, 3000 cents | |
| 247 | - | insert_transaction(&h.db, seller_id, item_id, 1000, "2026-03-03T12:00:00Z").await; | |
| 248 | - | insert_transaction(&h.db, seller_id, item_id, 2000, "2026-03-02T12:00:00Z").await; | |
| 261 | + | insert_transaction(&h.db, seller_id, item_id, 1000, &days_ago_at(2, 12)).await; | |
| 262 | + | insert_transaction(&h.db, seller_id, item_id, 2000, &days_ago_at(3, 12)).await; | |
| 249 | 263 | ||
| 250 | 264 | // Previous period (8-14 days ago): 1 sale, 500 cents | |
| 251 | - | insert_transaction(&h.db, seller_id, item_id, 500, "2026-02-24T12:00:00Z").await; | |
| 265 | + | insert_transaction(&h.db, seller_id, item_id, 500, &days_ago_at(10, 12)).await; | |
| 252 | 266 | ||
| 253 | 267 | // Period comparison with FILTER, user scope, 7d | |
| 254 | 268 | let row: (i64, i64, i64, i64) = sqlx::query_as( | |
| @@ -290,11 +304,11 @@ async fn follower_comparison() { | |||
| 290 | 304 | let seller_uuid: uuid::Uuid = seller_id.into(); | |
| 291 | 305 | ||
| 292 | 306 | // Current period follows (within last 30 days) | |
| 293 | - | insert_follow(&h.db, fan1, "user", seller_uuid, "2026-03-02T12:00:00Z").await; | |
| 294 | - | insert_follow(&h.db, fan2, "user", seller_uuid, "2026-03-01T12:00:00Z").await; | |
| 307 | + | insert_follow(&h.db, fan1, "user", seller_uuid, &days_ago_at(3, 12)).await; | |
| 308 | + | insert_follow(&h.db, fan2, "user", seller_uuid, &days_ago_at(5, 12)).await; | |
| 295 | 309 | ||
| 296 | 310 | // Previous period follow (31-60 days ago) | |
| 297 | - | insert_follow(&h.db, fan3, "user", seller_uuid, "2026-02-01T12:00:00Z").await; | |
| 311 | + | insert_follow(&h.db, fan3, "user", seller_uuid, &days_ago_at(35, 12)).await; | |
| 298 | 312 | ||
| 299 | 313 | // Follower comparison, user scope, 30d | |
| 300 | 314 | let row: (i64, i64) = sqlx::query_as( |