| 386 |
386 |
|
pub last_purchase_at: DateTime<Utc>,
|
| 387 |
387 |
|
}
|
| 388 |
388 |
|
|
|
389 |
+ |
/// A creator the fan has actively shared contact info with (no revocation).
|
|
390 |
+ |
#[derive(sqlx::FromRow)]
|
|
391 |
+ |
pub struct SharedCreatorRow {
|
|
392 |
+ |
pub seller_id: UserId,
|
|
393 |
+ |
pub username: String,
|
|
394 |
+ |
pub display_name: Option<String>,
|
|
395 |
+ |
}
|
|
396 |
+ |
|
| 389 |
397 |
|
/// Get unique contacts for a seller: buyers who opted in to share their email.
|
| 390 |
398 |
|
///
|
| 391 |
399 |
|
/// Aggregates across all completed transactions where `share_contact = true`,
|
| 392 |
|
- |
/// returning one row per buyer with purchase stats.
|
|
400 |
+ |
/// returning one row per buyer with purchase stats. Excludes buyers who have
|
|
401 |
+ |
/// revoked contact sharing.
|
| 393 |
402 |
|
pub async fn get_seller_contacts(
|
| 394 |
403 |
|
pool: &PgPool,
|
| 395 |
404 |
|
seller_id: UserId,
|
| 407 |
416 |
|
WHERE t.seller_id = $1
|
| 408 |
417 |
|
AND t.status = 'completed'
|
| 409 |
418 |
|
AND t.share_contact = true
|
|
419 |
+ |
AND NOT EXISTS (
|
|
420 |
+ |
SELECT 1 FROM contact_revocations cr
|
|
421 |
+ |
WHERE cr.buyer_id = t.buyer_id AND cr.seller_id = t.seller_id
|
|
422 |
+ |
)
|
| 410 |
423 |
|
GROUP BY t.buyer_id, u.username, u.email
|
| 411 |
424 |
|
ORDER BY last_purchase_at DESC
|
| 412 |
425 |
|
LIMIT 500
|
| 420 |
433 |
|
}
|
| 421 |
434 |
|
|
| 422 |
435 |
|
/// Get seller transactions for CSV export, with conditional buyer email.
|
|
436 |
+ |
///
|
|
437 |
+ |
/// Respects contact revocations: if a buyer revoked sharing, their email
|
|
438 |
+ |
/// is hidden even if `share_contact` was true on the transaction.
|
| 423 |
439 |
|
pub async fn get_seller_transactions_for_export(
|
| 424 |
440 |
|
pool: &PgPool,
|
| 425 |
441 |
|
seller_id: UserId,
|
| 432 |
448 |
|
t.item_title,
|
| 433 |
449 |
|
t.amount_cents,
|
| 434 |
450 |
|
t.status,
|
| 435 |
|
- |
CASE WHEN t.share_contact THEN u.email ELSE NULL END as buyer_email
|
|
451 |
+ |
CASE WHEN t.share_contact AND NOT EXISTS (
|
|
452 |
+ |
SELECT 1 FROM contact_revocations cr
|
|
453 |
+ |
WHERE cr.buyer_id = t.buyer_id AND cr.seller_id = t.seller_id
|
|
454 |
+ |
) THEN u.email ELSE NULL END as buyer_email
|
| 436 |
455 |
|
FROM transactions t
|
| 437 |
456 |
|
LEFT JOIN users u ON u.id = t.buyer_id
|
| 438 |
457 |
|
WHERE t.seller_id = $1
|
| 446 |
465 |
|
|
| 447 |
466 |
|
Ok(rows)
|
| 448 |
467 |
|
}
|
|
468 |
+ |
|
|
469 |
+ |
/// Record a contact revocation (fan withdraws email sharing from a creator).
|
|
470 |
+ |
///
|
|
471 |
+ |
/// Idempotent: does nothing if already revoked.
|
|
472 |
+ |
pub async fn revoke_contact_sharing(
|
|
473 |
+ |
pool: &PgPool,
|
|
474 |
+ |
buyer_id: UserId,
|
|
475 |
+ |
seller_id: UserId,
|
|
476 |
+ |
) -> Result<()> {
|
|
477 |
+ |
sqlx::query(
|
|
478 |
+ |
"INSERT INTO contact_revocations (buyer_id, seller_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
|
479 |
+ |
)
|
|
480 |
+ |
.bind(buyer_id)
|
|
481 |
+ |
.bind(seller_id)
|
|
482 |
+ |
.execute(pool)
|
|
483 |
+ |
.await?;
|
|
484 |
+ |
|
|
485 |
+ |
Ok(())
|
|
486 |
+ |
}
|
|
487 |
+ |
|
|
488 |
+ |
/// Clear a contact revocation (fan re-shares on a new purchase).
|
|
489 |
+ |
pub async fn clear_contact_revocation(
|
|
490 |
+ |
pool: &PgPool,
|
|
491 |
+ |
buyer_id: UserId,
|
|
492 |
+ |
seller_id: UserId,
|
|
493 |
+ |
) -> Result<()> {
|
|
494 |
+ |
sqlx::query(
|
|
495 |
+ |
"DELETE FROM contact_revocations WHERE buyer_id = $1 AND seller_id = $2",
|
|
496 |
+ |
)
|
|
497 |
+ |
.bind(buyer_id)
|
|
498 |
+ |
.bind(seller_id)
|
|
499 |
+ |
.execute(pool)
|
|
500 |
+ |
.await?;
|
|
501 |
+ |
|
|
502 |
+ |
Ok(())
|
|
503 |
+ |
}
|
|
504 |
+ |
|
|
505 |
+ |
/// Get creators the fan has actively shared contact info with (excluding revoked).
|
|
506 |
+ |
pub async fn get_shared_creators(
|
|
507 |
+ |
pool: &PgPool,
|
|
508 |
+ |
buyer_id: UserId,
|
|
509 |
+ |
) -> Result<Vec<SharedCreatorRow>> {
|
|
510 |
+ |
let rows = sqlx::query_as::<_, SharedCreatorRow>(
|
|
511 |
+ |
r#"
|
|
512 |
+ |
SELECT DISTINCT t.seller_id, u.username, u.display_name
|
|
513 |
+ |
FROM transactions t
|
|
514 |
+ |
JOIN users u ON u.id = t.seller_id
|
|
515 |
+ |
WHERE t.buyer_id = $1
|
|
516 |
+ |
AND t.status = 'completed'
|
|
517 |
+ |
AND t.share_contact = true
|
|
518 |
+ |
AND NOT EXISTS (
|
|
519 |
+ |
SELECT 1 FROM contact_revocations cr
|
|
520 |
+ |
WHERE cr.buyer_id = t.buyer_id AND cr.seller_id = t.seller_id
|
|
521 |
+ |
)
|
|
522 |
+ |
ORDER BY u.username
|
|
523 |
+ |
"#,
|
|
524 |
+ |
)
|
|
525 |
+ |
.bind(buyer_id)
|
|
526 |
+ |
.fetch_all(pool)
|
|
527 |
+ |
.await?;
|
|
528 |
+ |
|
|
529 |
+ |
Ok(rows)
|
|
530 |
+ |
}
|