feat(api): add the /api/v2 namespace and pricing-engine guards - #5779
Conversation
4f729da to
e658efc
Compare
e658efc to
fb040e8
Compare
fb040e8 to
6242c7c
Compare
6242c7c to
b06f00e
Compare
b06f00e to
7f6de41
Compare
7f6de41 to
79ea106
Compare
79ea106 to
c5663bb
Compare
c5663bb to
4f42553
Compare
3e14553 to
29ad20f
Compare
|
Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green HOLD — the REST billing split is not enforced by the sibling GraphQL API.
|
|
Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green HOLD — the route set cannot load, and the billing-mode boundary still has write-path bypasses.
|
|
Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green HOLD — legacy billing still has an unguarded GraphQL path, and false-valued legacy input bypasses catalog-plan validation.
|
|
Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green HOLD — the catalog boundary is still bypassable and the v2 fallback misroutes nested plan endpoints.
|
|
Automated pre-review (advisory, not a required check) — verdict: HOLD · CI green HOLD — legacy pricing still has a sibling write path through subscription plan overrides.
|
|
Automated pre-review (advisory, not a required check) — verdict: PASS · CI green PASS — The product-catalog and legacy-billing gates are consistently applied across REST, GraphQL, services, and model backstops, with focused coverage for routing parity, field translation, and attachment cleanup. Non-blocking: the new charge and fixed-charge validation examples should follow the project's consolidated model-validation spec structure. |
The product-catalog work introduces a second API version. A v2 client should use /api/v2 for its whole integration without mixing in v1, even for resources that have not been reimplemented yet. Extract the shared API resource definitions into a draw(:shared_api) file used by both namespaces. The v1 namespace draws them as today; a new v2 namespace draws the same set with module: :v1, so every /api/v2 path is served by the existing Api::V1 controller until a real Api::V2 controller replaces it. Add a routing parity spec asserting every v1 endpoint is reachable under v2, plus a request smoke test.
An organization is either on the product catalog (billing v2) or on legacy v1 pricing, never both. The engine is decided by the organization-level product_catalog feature flag, so it must not be a per-plan payload choice and both API surfaces must enforce the split. Wire the gating concerns into the API surfaces: v2 catalog endpoints require the organization flag (403 feature_unavailable without it) and v1 pricing write endpoints (plans charges, fixed charges, filters) are blocked for catalog organizations (403 legacy_billing_disabled), while v1 reads remain available for migration purposes. Derive Plan#pricing_type from the organization flag instead of accepting it in the payload, and remove it from the v1 plans endpoint. Add a v2 plans endpoint (create, update, show) with a catalog-only payload: no interval, amount_cents or pay_in_advance, since billing cadence lives on rate cards. New organizations enable the product_catalog feature flag on creation, so they use the catalog by default.
## Context GET /api/v2/plans fell through to the v1 fallback: it returned the heavy v1 payload and listed legacy plans that the v2 show action then rejects with a 404. The v2 surface exposed two different response shapes for the same resource. ## Description Add an index action to the v2 plans controller, scoped to product-catalog plans and serialized with the lean catalog shape used by show, create and update. The endpoint is gated by the product catalog flag like the rest of the catalog surface, and the fallback spec now demonstrates the v1 fallback on a resource without a v2 controller. The applied rate cards count goes through the preloaded association to avoid one count query per plan.
Context
An organization is either on the product catalog or on legacy v1 pricing, never both. The engine is an organization-level property, so it must not be a per-plan payload choice.
Description
The rule is enforced where it cannot be bypassed, as payload shape rather than blanket endpoint blocks:
Plans::CreateServiceandUpdateServicereject the legacy pricing fields (interval,amount_cents,pay_in_advance,charges,fixed_charges) for catalog organizations and plans withlegacy_billing_disabled; everything else (name, description, currency…) stays editable, and catalog plans stay deletable.ChargeandFixedChargevalidate that their plan is not a catalog plan — the deepest layer, closing internal paths such as subscription plan overrides that no endpoint gate can see.ForbidsLegacyBillingdoor-level 403 remains only on the pure-legacy charge, fixed-charge and charge-filter endpoints and mutations, where every action is illegal for a catalog org.RequiresProductCataloggates the catalog surface on both APIs: v2 REST endpoints, catalog GraphQL mutations and resolvers (403feature_unavailablewithout the flag).CreatePlanInput/UpdatePlanInputstay unchanged for legacy, and catalog organizations get dedicatedcreateCatalogPlan/updateCatalogPlanmutations with a catalog-only payload exposing the plan currency ascurrency.destroyPlanis shared. Rate cards attach to catalog plans only (legacy_planotherwise)./api/v2namespace, falling back to v1 for every path it does not define, kept exhaustive by a routing parity spec, plus a v2 plans endpoint with the same catalog-only payload.Plan#pricing_typefrom the organization flag and remove it from the v1 payload.Defaulting new organizations to the catalog moves to #5871, alongside the v2 subscription surface: enabling it here would let a fresh organization create catalog plans while its subscriptions still go through the ungated v1 endpoint into a runtime that cannot bill them.