feat(stripe): price-scoped subscription count, price unarchive, interval_count on price response #41

Merged
longjacksonle merged 1 commit from feat/DUNITE-9-price-subscription-count into main 2026-08-12 03:44:27 +02:00

DUNITE-9. Three additions to the price surface of dunite-stripe, all driven by the consumers' admin billing screens.

count_price_subscriptions

Counts subscriptions attached to one price using Stripe's server-side price list filter, so a monthly and a yearly price on the same product report separately rather than collapsing into a product-level number. Status filtering is Stripe's default (every subscription it does not consider canceled: active, trialing, past_due, unpaid, paused, incomplete), which is the set an admin cares about before archiving a price.

Pages at 100 per request up to MAX_PRICE_SUBSCRIPTION_PAGES (20 pages = 2000 subscriptions) instead of walking unbounded and hanging an admin request on sequential Stripe round trips. Hitting the cap sets complete: false on the new StripePriceSubscriptionCount and logs a warning, so the caller can render "N+" rather than a silently wrong total.

unarchive_price

Inverse of archive_price: UpdatePrice { active: Some(true) }. Stripe refuses to activate a price whose parent product is still archived, so the doc comment tells callers to unarchive the product first and the error log carries that as a hint field.

recurring_interval_count on StripePriceResponse

Read off recurring.interval_count in both list_prices and create_price. Without it the DTO cannot represent a quarterly price: interval = month alone is indistinguishable from monthly billing. None for one-off prices. create_price reads the value back off Stripe's response rather than assuming the default of 1.

Compatibility

Additive on both the DTO and the service. Consumers that deserialize or read StripePriceResponse are unaffected; a consumer that constructs the struct literally must add the new field.

Testing

just fmt, just check, just lint and just test are clean (cargo test --workspace run four times end to end). One intermediate sweep hit a load-timing flake in an unrelated crate (dunite-download's burst_of_concurrent_stores_coalesces_into_few_eviction_passes_and_converges); it passes standalone and in every subsequent sweep on this branch, and nothing here touches that crate.

No new tests: every added path is a live Stripe API round trip and the crate has no HTTP-level harness (the async-stripe client's base URL is not injectable here), consistent with the rest of the service.

DUNITE-9. Three additions to the price surface of `dunite-stripe`, all driven by the consumers' admin billing screens. ## `count_price_subscriptions` Counts subscriptions attached to one price using Stripe's server-side `price` list filter, so a monthly and a yearly price on the same product report separately rather than collapsing into a product-level number. Status filtering is Stripe's default (every subscription it does not consider canceled: `active`, `trialing`, `past_due`, `unpaid`, `paused`, `incomplete`), which is the set an admin cares about before archiving a price. Pages at 100 per request up to `MAX_PRICE_SUBSCRIPTION_PAGES` (20 pages = 2000 subscriptions) instead of walking unbounded and hanging an admin request on sequential Stripe round trips. Hitting the cap sets `complete: false` on the new `StripePriceSubscriptionCount` and logs a warning, so the caller can render "N+" rather than a silently wrong total. ## `unarchive_price` Inverse of `archive_price`: `UpdatePrice { active: Some(true) }`. Stripe refuses to activate a price whose parent product is still archived, so the doc comment tells callers to unarchive the product first and the error log carries that as a `hint` field. ## `recurring_interval_count` on `StripePriceResponse` Read off `recurring.interval_count` in both `list_prices` and `create_price`. Without it the DTO cannot represent a quarterly price: `interval = month` alone is indistinguishable from monthly billing. `None` for one-off prices. `create_price` reads the value back off Stripe's response rather than assuming the default of 1. ## Compatibility Additive on both the DTO and the service. Consumers that deserialize or read `StripePriceResponse` are unaffected; a consumer that constructs the struct literally must add the new field. ## Testing `just fmt`, `just check`, `just lint` and `just test` are clean (`cargo test --workspace` run four times end to end). One intermediate sweep hit a load-timing flake in an unrelated crate (`dunite-download`'s `burst_of_concurrent_stores_coalesces_into_few_eviction_passes_and_converges`); it passes standalone and in every subsequent sweep on this branch, and nothing here touches that crate. No new tests: every added path is a live Stripe API round trip and the crate has no HTTP-level harness (the `async-stripe` client's base URL is not injectable here), consistent with the rest of the service.
feat(stripe): price subscription count, unarchive, interval_count
All checks were successful
Check / fmt + clippy + test (pull_request) Successful in 53s
create-release / create-release (pull_request) Has been skipped
a7872fc899
Three additions to dunite-stripe's price surface (DUNITE-9), all needed by the admin billing screens in the consumers and none of them expressible without a service-layer call.

`StripeService::count_price_subscriptions` counts the subscriptions attached to one price via Stripe's server-side `price` list filter, so two prices on the same product (monthly vs yearly) report separately instead of collapsing into a product-level number. Status filtering is Stripe's default: every subscription it does not consider canceled, which is the set that matters before archiving a price. It pages at 100 per request up to `MAX_PRICE_SUBSCRIPTION_PAGES` (20, so 2000 subscriptions) rather than making an admin request wait on an unbounded walk; hitting that cap returns `complete: false` on the new `StripePriceSubscriptionCount` so the caller can render "N+" instead of silently reporting a wrong total.

`StripeService::unarchive_price` is the inverse of `archive_price` (`UpdatePrice { active: Some(true) }`). Stripe rejects activating a price whose parent product is still archived, so the failure log carries that as a hint and the doc comment says to unarchive the product first.

`StripePriceResponse` gains `recurring_interval_count`, read off `recurring.interval_count` in both `list_prices` and `create_price`. Without it the DTO cannot describe a quarterly price: `interval = month` alone is indistinguishable from monthly billing. `None` for one-off prices. This is an additive field on a response DTO, so consumers that only deserialize or read it are unaffected; any consumer constructing the struct literally needs the extra field.

No tests: every new path is a Stripe API round trip and the crate has no HTTP-level test harness (async-stripe's client base URL is not injectable here), matching the rest of the service.
longjacksonle deleted branch feat/DUNITE-9-price-subscription-count 2026-08-12 03:44:28 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/dunite!41
No description provided.