Skip to main content

max / makenotwork

Let the buyer choose who converts, and the collaborator agree to their split Three things the settlement-currency work left owing: the buyer had no say in how a cross-currency purchase was converted, the docs still said USD-only, and a creator could be given a revenue share in a currency they do not hold without ever being asked. THE BUYER'S CHOICE. A radio pair on the cart and a checkbox on the cross-seller bar, stored as a preference so a returning buyer is not asked twice, and carried in the session so a chained multi-seller run keeps the choice it started with. An unparseable form value reads as convert-at- checkout: a mangled value must not silently drop someone onto the path where the cost is invisible until their statement arrives. The task specified showing this only when the buyer's currency differs from the seller's. That is not knowable. There is no geo, locale or IP signal in the codebase, and settlement_currency defaults to USD for every account, so it is evidence of a buyer's currency only once they have connected Stripe; treating the default as fact would hide the control from exactly the UK fan who needs it. So it hides only on positive knowledge of a match and is worded conditionally otherwise. WHAT CAN HONESTLY BE SHOWN. Before payment, the published 2-4% range and the fact that Stripe displays the exact converted total on its own page. After payment, the exact figure: migration 193 stores what Stripe actually charged, read from presentment_details on the completed webhook, and the receipt renders it. Both columns or neither, by CHECK. The presentment currency is deliberately unconstrained, unlike the six we settle in -- Stripe presents in 150+ markets and this column records what happened rather than what we chose. THE SPLIT HAS TO BE ACCEPTED. Migration 194 makes a membership pending until the collaborator agrees. Pending reserves the percentage, so an owner cannot promise the same revenue twice, but earns nothing: a share starts at acceptance and is not backdated, because accruing money for someone who may yet decline invents a debt that is worse to unwind than never to have made. Declining frees the percentage. Existing members are backdated to added_at, since being added was the agreement under the old rules and leaving them pending would have switched off every live split. That gate is what gives the currency disclosure somewhere to live. The collaborator sees, before agreeing, that their share arrives in the project's currency and that Stripe converts it at their payout out of their share; the owner sees the same when they invite. Neither could be told earlier, because there was no earlier. Found on the way: create_transaction_splits and create_tip_splits never wrote the currency column migration 190 added, so every new split would have taken the 'usd' default whatever the sale was in. Also fixed, all the same class of silent USD: item.html published priceCurrency "USD" in JSON-LD, which machines read and would misprice a non-USD creator's work in search results; the fee breakdown quoted US card pricing at everyone, and is now shown only where it is true; the disclaimer invented a $15 chargeback fee, replaced by a pointer to Stripe's own pricing for the reader's country; and the payments tab tested splits_outgoing_total != "$0.00" as a zero check. The per-seller analytics time-series still sums a creator whose settlement currency changed. Documented at the query rather than papered over: splitting a chart per currency is a chart-design decision, not a query fix.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Author: Max Johnson <me@maxj.phd> · 2026-08-06 23:56 UTC
Signed with PGP, not checked
Commit: 14bd66bbe5867a2e912cd0899aff95f1fd64bb5f
Parent: 57a3bf8
57 files changed, +1617 insertions, -442 deletions
@@ -470,6 +470,40 @@
470 470 }
471 471 }
472 472
473 + // ── ConversionChoice ──
474 +
475 + #[test]
476 + fn a_mangled_form_value_lands_on_the_visible_path() {
477 + // Anything unrecognised must default to convert-at-checkout, the path
478 + // where the buyer sees the total before paying. Defaulting the other way
479 + // would hide the cost behind a rate we cannot show.
480 + for raw in [None, Some(""), Some("nonsense"), Some("CHECKOUT")] {
481 + assert_eq!(
482 + ConversionChoice::from_form_value(raw),
483 + ConversionChoice::AtCheckout,
484 + "{raw:?}"
485 + );
486 + }
487 + assert_eq!(
488 + ConversionChoice::from_form_value(Some("bank")),
489 + ConversionChoice::ByBuyersBank
490 + );
491 + }
492 +
493 + #[test]
494 + fn the_choice_round_trips_through_the_form_and_the_column() {
495 + for choice in [ConversionChoice::AtCheckout, ConversionChoice::ByBuyersBank] {
496 + assert_eq!(ConversionChoice::from_db(choice.as_form_value()), choice);
497 + }
498 + }
499 +
500 + #[test]
501 + fn only_convert_at_checkout_turns_adaptive_pricing_on() {
502 + // The flag is the entire mechanism, so the mapping must not drift.
503 + assert!(ConversionChoice::AtCheckout.adaptive_pricing_enabled());
504 + assert!(!ConversionChoice::ByBuyersBank.adaptive_pricing_enabled());
505 + }
506 +
473 507 // ── MoneyByCurrency ──
474 508
475 509 fn money(rows: &[(SettlementCurrency, i64)]) -> MoneyByCurrency {