feat(admin): unarchive a Stripe product or price from the admin Stripe page (BUNYIP-513) #515
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/BUNYIP-513-unarchive-stripe-plan"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Stacked on BUNYIP-512
This branch is stacked on
fix/BUNYIP-512-archive-cascade-and-member-guard(bunyip PR for BUNYIP-512), because both touch the same admin Stripe handler, block, audit enum and test regions. Until BUNYIP-512 merges, this PR's diff shows both commits; once it merges to main, only the BUNYIP-513 commit remains. Merge BUNYIP-512 first.What and why
Archiving was one-way in the admin Stripe page (BUNYIP-513).
stripe_products_block/stripe_prices_blockrendered an action only for active rows; an archived row had none, so undoing an accidental archive meant logging into the Stripe dashboard, exactly the split the admin page exists to avoid. Stripe itself treats archive as reversible.What changed
POST /v1/admin/stripe/products/{id}/unarchivesets the productactive = trueviaupdate_product(id, None, None, None, Some(true)). Metadata is not sent, soupdate_productleaves the map intact and the app tag survives (the same trap the update path documents).POST /v1/admin/stripe/prices/{id}/unarchiverestores a single price viaStripeService::unarchive_price.bunyip-web/src/api/admin.rs) and handlers mirror the archive ones, keeping the toast-on-error posture (upstream message ontoast_err, never a silent success redirect).AdminStripePlanUnarchived(admin_stripe_plan_unarchived) entry naming the restored id.audit_logs.actionis free-text, so no migration.dunite bump
StripeService::unarchive_priceshipped in dunite-stripe commita7872fc(already on dunite main), so #DUNITE-9 is done upstream: no dunite PR, just a rev bump.dunite-stripeanddunite-stripe-coremove togetherae62c27->5dab94d(they are a path-linked pair, so a single rev keeps onedunite-stripe-core; its source is byte-identical across the range). Member-scopedcargo update --package dunite-stripe --package dunite-stripe-core, a 2-lineCargo.lockdiff.That range also (DUNITE-10) added a classified
StripeServiceError::Stripe { message, details }variant and arecurring_interval_countfield onStripePriceResponse. The sharedstripe_errmapper now handles the new variant (resource_missing -> 404, else 500, per the variant's doc), and the one affected test constructor sets the new field. These are the only ripples from the bump.Deviation from the ticket
The ticket assumed #DUNITE-9 still needed to land and this PR would introduce it. It was already merged upstream, so this is a plain rev bump, not a cross-repo change.
Tests
unarchive_product_sends_active_true_and_no_metadataasserts the handler callsupdate_productwith metadataNoneand activeSome(true)(AC: no metadata key).unarchive_product_issues_no_price_updateasserts the product path touches no price call (AC: unarchive does not change price state).unarchive_price_restores_the_one_priceasserts the single-price path.stripe_err_maps_the_classified_stripe_variantcovers the new error mapping.products_block_offers_unarchive_for_archived_only/prices_block_offers_unarchive_for_archived_onlycover the Unarchive control on archived rows and its absence on active rows, plus the "becomes purchasable again" copy.pricing_cacheinvalidation scan now includesunarchive, so a future price-unarchive handler that forgets to drop the cache fails the build.just check-containergreen (fmt + clippy + full workspace tests). No doc described archiving as one-way, so none needed updating.🤖 Generated with Claude Code
https://claude.ai/code/session_01GkNbvEq6awuMRULFCiYKe3
Archiving was one-way in the admin Stripe page: an archived product or price row had no action, so undoing an accidental archive meant logging into the Stripe dashboard, exactly the split the admin page exists to avoid. This adds the reverse transition. `POST /v1/admin/stripe/products/{id}/unarchive` sets the product `active = true` via `update_product(id, None, None, None, Some(true))`. Metadata is deliberately not sent, so the whole metadata map is left intact and the app tag survives. `POST /v1/admin/stripe/prices/{id}/unarchive` restores a single price via `StripeService::unarchive_price`. There is no cascade on unarchive, on purpose. The archive direction (BUNYIP-512) cascades a product to its prices, but the reverse is not symmetric: the set of prices a given archive cascade touched is not recorded, and a blanket restore would resurrect prices archived for unrelated reasons (a superseded price, for one). Restoring a product restores only the product; each archived price is restored explicitly on its own row. An unarchived product with no active price is already flagged by BUNYIP-512's "No active price" warning, whose fix is the per-price Unarchive control this adds. Neither path is member-guarded: restoring a plan withdraws nothing. In the Products and Prices blocks an archived row now renders an Unarchive control where the Archive control sits for active rows, with a confirmation stating the plan becomes purchasable again. Web routes, client functions and handlers mirror the archive ones, keeping the toast-on-error posture (the upstream message rides `toast_err`, never a silent success redirect). Both paths write an `AdminStripePlanUnarchived` audit entry naming the restored id. `unarchive_price` ships in dunite-stripe as of a7872fc (already on dunite main), so this bumps the `dunite-stripe` / `dunite-stripe-core` rev ae62c27 -> 5dab94d (the two move together as a single dunite-stripe-core; stripe-core source is byte-identical across the range) with the member-scoped `cargo update` (a 2-line Cargo.lock diff). That range also added the classified `StripeServiceError::Stripe { .. }` variant and a `recurring_interval_count` field on `StripePriceResponse`; the shared `stripe_err` mapper now handles the new variant (resource_missing -> 404, else 500) and the one affected test constructor is updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GkNbvEq6awuMRULFCiYKe3