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
Apply a SyncKit cap increase now, not at the next renewal
Every cap change queued to pending_storage_limit_bytes and waited for the
renewal webhook, while update_synckit_app_sub_price re-quoted Stripe
immediately with prorations. So raising a cap charged the higher price at
once and handed over the storage up to a month later, and the user doing
it is usually one already blocked by a full cap: they pay to be unblocked
and stay blocked.
Increases now set storage_limit_bytes directly and clear any pending
change. Decreases still queue, which is the same asymmetry the proration
already implies: the user paid for the period they are in and keeps it
until the period ends.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
2 files changed,
+54 insertions,
-3 deletions
226
226
Ok(())
227
227
}
228
228
229
+
/// Raise the active cap now, clearing any queued change.
230
+
///
231
+
/// For increases only, and the asymmetry is the point. The price is re-quoted
232
+
/// against Stripe immediately with prorations, so the user is charged for the
233
+
/// larger cap the moment they ask for it; pending that to the next renewal
234
+
/// would take the money now and hand over the storage up to a month later,
235
+
/// which is the state a user hits precisely when they are already blocked by a
236
+
/// full cap. Decreases still queue via `set_pending_storage_cap`: the user paid
237
+
/// for the period they are in and keeps it until the period ends.
238
+
#[tracing::instrument(skip_all)]
239
+
pub async fn set_storage_cap_now(
240
+
pool: &PgPool,
241
+
user_id: UserId,
242
+
app_id: SyncAppId,
243
+
cap_bytes: i64,
244
+
) -> Result<()> {
245
+
sqlx::query(
246
+
r"
247
+
UPDATE app_sync_subscriptions
248
+
SET storage_limit_bytes = $3,
249
+
pending_storage_limit_bytes = NULL
250
+
WHERE user_id = $1 AND app_id = $2
251
+
",
252
+
)
253
+
.bind(user_id)
254
+
.bind(app_id)
255
+
.bind(cap_bytes)
256
+
.execute(pool)
257
+
.await?;
258
+
Ok(())
259
+
}
260
+
229
261
/// Promote a queued cap change to the active cap. Called from the renewal
230
262
/// webhook handler when Stripe rolls the subscription to a new period.
231
263
/// No-op if no pending change is queued.
453
453
)
454
454
.await?;
455
455
456
-
db::synckit::set_pending_storage_cap(&db, sync_user.user_id, sync_user.app_id, req.cap_bytes)
456
+
// An increase takes effect now, a decrease at the next renewal. Stripe was
457
+
// just re-priced with prorations either way, so queueing an increase would
458
+
// charge for headroom the user cannot use until the period rolls - and a
459
+
// user raising their cap is usually one already blocked by a full one.
460
+
let raising = req.cap_bytes > sub.storage_limit_bytes.unwrap_or(0);
461
+
if raising {
462
+
db::synckit::set_storage_cap_now(&db, sync_user.user_id, sync_user.app_id, req.cap_bytes)
463
+
.await?;
464
+
} else {
465
+
db::synckit::set_pending_storage_cap(
466
+
&db,
467
+
sync_user.user_id,
468
+
sync_user.app_id,
469
+
req.cap_bytes,
470
+
)
457
471
.await?;
472
+
}
458
473
459
474
Ok(Json(SyncSubscriptionStatusResponse {
460
475
active: sub.status == "active",
461
476
tier: Some(sub.interval),
462
477
status: Some(sub.status),
463
-
storage_limit_bytes: sub.storage_limit_bytes,
464
-
pending_storage_limit_bytes: Some(req.cap_bytes),
478
+
storage_limit_bytes: if raising {
479
+
Some(req.cap_bytes)
480
+
} else {
481
+
sub.storage_limit_bytes
482
+
},
483
+
pending_storage_limit_bytes: (!raising).then_some(req.cap_bytes),
465
484
storage_used_bytes: Some(
466
485
db::synckit::storage_used_bytes(&db, sync_user.app_id, sync_user.user_id).await?,
467
486
),