main
tag: launch-2026-06-01
tag: magicmirror-v0.1.1
tag: magicmirror-v0.3.0
tag: mnw-cli-v0.1.2
tag: mnw-cli-v0.1.3
tag: mnw-cli-v0.1.4
tag: pom-v0.4.1
tag: pom-v0.4.2
tag: pom-v0.4.3
tag: pom-v0.4.4
tag: pom-v0.4.5
tag: wam-v0.3.0
tag: wam-v0.3.1
Files
Commits
Tags
Notes
Issues
payments: seal StripeAccountId + cart ItemType, add DB-layer tests
Drive Payments A- -> A+ (ultra-fuzz Run 7 --deep).
Codebase-wide StripeAccountId newtype: the in-memory and DB representation
of a creator's connected account across every payout path (DbUser,
DbProjectMemberWithUser, CartItem, the User view type), converting to &str
only at the Stripe wire boundary. create_connect_account and the fan-out
helper carry the newtype; validate_stripe_account_id checks the acct_ shape
at input boundaries. Retires the bare String that let any string stand in
for a connected-account id.
Seal CartItem.item_type as the existing ItemType enum (the lone holdout;
the type already backs items/discover/tags).
DB-layer tests for the Payments cold-spot modules (tips/license_keys/
pending_refunds had zero direct coverage): pending-refund claim/unclaim/
complete lifecycle + crash-window sweep, tip create-guard + complete/refund
idempotency, license-key finalize idempotency + activation-cap enforcement.
Widen those three db modules to pub mod so the integration crate can pin
their contracts directly.
Gate: cargo clippy --all-targets clean, cargo test --lib (1764),
cargo test --test integration (983). sqlx offline unchanged (no macro SQL
touched). No version bump, no deploy.
Co-Authored-By
Claude Opus 4.8 <noreply@anthropic.com>
19 files changed,
+336 insertions,
-26 deletions
3
3
use chrono::{DateTime, Duration, Utc};
4
4
use sqlx::PgPool;
5
5
6
-
use super::{ItemId, ProjectId, UserId};
6
+
use super::validated_types::StripeAccountId;
7
+
use super::{ItemId, ItemType, ProjectId, UserId};
7
8
use crate::error::Result;
8
9
9
10
135
136
/// a `get_item_by_id` per item at checkout.
136
137
pub project_id: ProjectId,
137
138
pub title: String,
138
-
pub item_type: String,
139
+
pub item_type: ItemType,
139
140
pub price_cents: i32,
140
141
pub pwyw_enabled: bool,
141
142
pub pwyw_min_cents: Option<i32>,
143
144
pub amount_cents: Option<i32>,
144
145
pub creator_username: String,
145
146
pub seller_id: UserId,
146
-
pub seller_stripe_account_id: Option<String>,
147
+
pub seller_stripe_account_id: Option<StripeAccountId>,
147
148
pub seller_charges_enabled: bool,
148
149
pub project_slug: String,
149
150
pub added_at: DateTime<Utc>,
333
334
item_id: ItemId::nil(),
334
335
project_id: ProjectId::nil(),
335
336
title: String::new(),
336
-
item_type: String::from("audio"),
337
+
item_type: ItemType::Audio,
337
338
price_cents,
338
339
pwyw_enabled,
339
340
pwyw_min_cents,
23
23
pub(crate) mod auth;
24
24
pub mod waitlist;
25
25
pub(crate) mod blog_posts;
26
-
pub(crate) mod license_keys;
26
+
pub mod license_keys;
27
27
pub mod synckit; // pub so the integration test crate can exercise compaction directly
28
28
pub mod synckit_billing;
29
29
pub(crate) mod oauth;
62
62
pub(crate) mod email_signups;
63
63
pub(crate) mod imports;
64
64
pub(crate) mod media_files;
65
-
pub(crate) mod tips;
65
+
pub mod tips;
66
66
pub(crate) mod project_members;
67
67
pub mod idempotency; // pub so the integration test crate can exercise it directly
68
-
pub(crate) mod pending_refunds;
68
+
pub mod pending_refunds;
69
69
pub mod webhook_events;
70
70
pub(crate) mod scheduler_jobs;
71
71
pub(crate) mod moderation;
119
119
KeyCode => crate::validation::validate_key_code,
120
120
Slug => crate::validation::validate_slug,
121
121
Username => crate::validation::validate_username,
122
+
// StripeAccountId: Stripe Connect account ID (`acct_...`). The in-memory and
123
+
// DB representation of a creator's connected account across every payout
124
+
// path; convert to `&str` (via `Deref`/`as_str`) only at the Stripe wire
125
+
// boundary. `new()` validates the `acct_` shape; DB reads use `from_trusted`
126
+
// (Stripe is the source of truth for the stored value).
127
+
StripeAccountId => crate::validation::validate_stripe_account_id,
122
128
);
123
129
124
130
// ── Email ──
16
16
use stripe_product::price::{CreatePrice, CreatePriceRecurring, CreatePriceRecurringInterval};
17
17
use stripe_types::Currency;
18
18
19
+
use crate::db::StripeAccountId;
19
20
use crate::error::{AppError, Result};
20
21
use super::StripeClient;
21
22
28
29
impl StripeClient {
29
30
/// Create a Stripe Standard connected account for a creator.
30
31
#[tracing::instrument(skip_all, name = "payments::create_connect_account")]
31
-
pub async fn create_connect_account(&self, email: &str) -> Result<String> {
32
+
pub async fn create_connect_account(&self, email: &str) -> Result<StripeAccountId> {
32
33
let account = CreateAccount::new()
33
34
.type_(CreateAccountType::Standard)
34
35
.email(email.to_string())
38
39
tracing::error!(error = ?e, "failed to create Stripe connected account");
39
40
AppError::BadRequest("Failed to create Stripe account".to_string())
40
41
})?;
41
-
Ok(account.id.to_string())
42
+
// Stripe minted this id; trust its shape rather than re-validating.
43
+
Ok(StripeAccountId::from_trusted(account.id.to_string()))
42
44
}
43
45
44
46
/// Create an Account Link for Stripe Connect onboarding.
13
13
use std::sync::Arc;
14
14
15
15
use crate::background::BackgroundTx;
16
+
use crate::db::StripeAccountId;
16
17
use crate::payments::PaymentProvider;
17
18
18
19
/// Which Stripe subscription operation to apply to each fan subscription.
62
63
pub fn spawn_fan_sub_fanout(
63
64
bg: &BackgroundTx,
64
65
stripe: Arc<dyn PaymentProvider>,
65
-
account_id: String,
66
+
account_id: StripeAccountId,
66
67
sub_ids: Vec<String>,
67
68
op: FanSubOp,
68
69
) {
73
74
let total = sub_ids.len();
74
75
let mut failed = 0u32;
75
76
for sub_id in &sub_ids {
76
-
if let Err(e) = op.apply(&stripe, sub_id, &account_id).await {
77
+
if let Err(e) = op.apply(&stripe, sub_id, account_id.as_str()).await {
77
78
failed += 1;
78
79
tracing::warn!(stripe_sub_id = %sub_id, op = op.label(), error = ?e, "fan subscription op failed");
79
80
}
88
88
async fn create_cart_checkout_session(&self, params: &CartCheckoutParams<'_>) -> crate::error::Result<CheckoutResult>;
89
89
90
90
// Connect
91
-
async fn create_connect_account(&self, email: &str) -> crate::error::Result<String>;
91
+
async fn create_connect_account(&self, email: &str) -> crate::error::Result<crate::db::StripeAccountId>;
92
92
async fn create_account_link(&self, account_id: &str, return_url: &str, refresh_url: &str) -> crate::error::Result<String>;
93
93
async fn fetch_account(&self, account_id: &str) -> crate::error::Result<AccountUpdate>;
94
94
async fn create_subscription_product_and_price(&self, connected_account_id: &str, tier_name: &str, tier_description: Option<&str>, price_cents: i64) -> crate::error::Result<(String, String)>;
177
177
Ok(CheckoutResult { id: session.id.to_string(), url: session.url })
178
178
}
179
179
180
-
async fn create_connect_account(&self, email: &str) -> crate::error::Result<String> {
180
+
async fn create_connect_account(&self, email: &str) -> crate::error::Result<crate::db::StripeAccountId> {
181
181
StripeClient::create_connect_account(self, email).await
182
182
}
183
183
1
1
//! View types for user-facing surfaces: profile, waitlist, invites.
2
2
3
3
use super::json_escape;
4
+
use crate::db::StripeAccountId;
4
5
5
6
/// User profile data
6
7
#[derive(Clone)]
13
14
pub avatar_url: Option<String>,
14
15
// Stripe Connect status
15
16
pub stripe_connected: bool,
16
-
pub stripe_account_id: Option<String>,
17
+
pub stripe_account_id: Option<StripeAccountId>,
17
18
pub stripe_onboarding_complete: bool,
18
19
pub stripe_payouts_enabled: bool,
19
20
pub stripe_charges_enabled: bool,