Skip to main content

max / makenotwork

Raise a ticket when a creator's settlement currency changes Every price is stored as a bare number, so a Stripe account moving from GBP to EUR silently redenominates the lot. Nothing is converted or rewritten here, because guessing a rate is what this design refuses to do; a person is told instead. Reads the stored currency before the write so a real change can be told from Stripe restating the same value on one of its many account.updated events.
Author: Max Johnson <me@maxj.phd> · 2026-08-07 00:19 UTC
Signed with PGP, not checked
Commit: edf7bb94a213638bf43a3faccf22a104ed0f9104
Parent: 00793b4
2 files changed, +65 insertions, -0 deletions
@@ -522,6 +522,25 @@
522 522 Ok(user)
523 523 }
524 524
525 + /// The settlement currency currently stored for a connected account, if any.
526 + ///
527 + /// Read before a webhook write so a *change* can be distinguished from a
528 + /// restatement of the same value. Stripe re-sends `account.updated` constantly,
529 + /// so alerting on every write would be noise; alerting on none of them would
530 + /// leave a creator's prices silently meaning different money.
531 + #[tracing::instrument(skip_all)]
532 + pub async fn get_settlement_currency_by_stripe_account(
533 + pool: &PgPool,
534 + stripe_account_id: &str,
535 + ) -> Result<Option<crate::currency::SettlementCurrency>> {
536 + let row: Option<(crate::currency::SettlementCurrency,)> =
537 + sqlx::query_as("SELECT settlement_currency FROM users WHERE stripe_account_id = $1")
538 + .bind(stripe_account_id)
539 + .fetch_optional(pool)
540 + .await?;
541 + Ok(row.map(|(c,)| c))
542 + }
543 +
525 544 /// Store a buyer's cross-currency conversion preference.
526 545 ///
527 546 /// A preference, not a lock: the checkout form decides each purchase, and this
@@ -345,6 +345,14 @@
345 345 "account updated"
346 346 );
347 347
348 + // Read the stored currency before overwriting it, so a genuine change can be
349 + // told apart from Stripe restating the same value on one of the many
350 + // `account.updated` events it sends.
351 + let previous_currency =
352 + db::users::get_settlement_currency_by_stripe_account(db, &update.account_id)
353 + .await
354 + .unwrap_or(None);
355 +
348 356 // Update the user's Stripe status
349 357 db::users::update_user_stripe_status(
350 358 db,
@@ -357,6 +365,44 @@
357 365 .await
358 366 .with_context(|| format!("update Stripe status for account {}", update.account_id))?;
359 367
368 + // A settlement currency change is not a status change: it silently
369 + // redenominates every price the creator has set. Their 1000 was ten pounds
370 + // and is now ten euros, and only they can decide what the number should be.
371 + // Nothing here rewrites their prices, because guessing at a rate is exactly
372 + // what this design refuses to do; it raises the alarm so a person acts.
373 + //
374 + // This ticket alerts MNW. Telling the *creator*, and requiring them to
375 + // acknowledge it, is mnw-server task 5522bcbf, which names this as its
376 + // first caller.
377 + if let (Some(new_currency), Some(old_currency)) =
378 + (update.settlement_currency, previous_currency)
379 + && new_currency != old_currency
380 + {
381 + tracing::warn!(
382 + account_id = %update.account_id,
383 + %old_currency, %new_currency,
384 + "settlement currency changed; the creator's existing prices now mean different money"
385 + );
386 + if let Some(wam) = wam {
387 + let title = format!("Settlement currency changed: {}", update.account_id);
388 + let body = format!(
389 + "This creator's Stripe account moved from {old_currency} to {new_currency}.\n\n\
390 + Every price they have already set is stored as a bare number, so those \
391 + numbers now mean {new_currency} instead of {old_currency}. Nothing has been \
392 + converted and nothing has been rewritten.\n\n\
393 + They need to re-check their prices. Contact them."
394 + );
395 + wam.create_ticket(
396 + &title,
397 + Some(&body),
398 + "high",
399 + "settlement-currency-changed",
400 + Some(&update.account_id),
401 + )
402 + .await;
403 + }
404 + }
405 +
360 406 // Alert if charges or payouts became disabled (creator can't receive payments)
361 407 if (!update.charges_enabled || !update.payouts_enabled)
362 408 && let Some(wam) = wam