feat(subscriptions): add get, update, pause and resume (#78) - #115
Merged
Merged
Conversation
Completes the watch lifecycle against /v1/subscriptions. get/update/pause/
resume return the Subscription unwrapped from { subscription }, validate the
id and payload before sending, and keep 402/422 recovery details on rawBody.
update() refuses a status other than active/paused, which the API answers
with HTTP 500 (api#8471). A timed-out PATCH is not replayed.
list() no longer turns a missing subscriptions key into []; it and create()
raise unexpected_response_shape for a body they cannot map.
Tests drive the real client through a fetch mock over fixtures captured from
a live lifecycle run on the test key's own account (watch deleted in finally).
The live smoke adds the lifecycle, pausing right after create and deleting in
finally; it ran against production and passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015ao5paex73xXvuM424Libo
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID: Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
This was referenced Sep 13, 2026
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #78
What
/v1/subscriptionsis (established before any live call)On
oilpriceapi-apiorigin/main,/v1/subscriptionsisV1::SubscriptionsControllerover theWatchmodel: agent "watches" that snapshot a set of commodity codes on an interval. It is deliberately separate fromBilling::Subscription.pausecalls@watch.paused!,resumecalls@watch.active!plusarm_now!, andupdateacceptsname, interval_seconds, deliver_webhook, status, codes. None of them touches Stripe or billing.WatchEvaluatorWorkerdispatches due watches toWatchSnapshotWorker, which writes aWatchEventand advancesnext_run_at. It records noapi_requestsrow, so a live watch consumes no request quota between calls. CRUD calls count as normal requests, andeventsis excluded from quota (skip_after_action :track_request).WatchLimitPolicy#can_create?compares the count of active watches withApiLimits::WATCH_LIMITS(free 1 … enterprise 1,000). A paused watch does not count.Watch#interval_at_or_above_tier_floorenforcesMIN_INTERVAL_SECONDS(free 3,600s … 60s). An over-limit create returns 402 with an upgrade block; an update below the floor returns 422VALIDATION_ERROR.No member action changes what a customer is billed, so the live smoke was run.
Observed wire shapes (live, 2026-09-13, test key's own account)
POST /v1/subscriptions{status:"success", data:{subscription:{id, name, codes, interval_seconds, status:"active", deliver_webhook, source, tool_name, last_evaluated_at:null, next_run_at, created_at}}}POST /v1/subscriptions/:id/pausestatus:"paused"GET /v1/subscriptions/:idPATCH /v1/subscriptions/:id {name}PATCH … {interval_seconds:1}{status:"fail", data:{error:"VALIDATION_ERROR", message:"Interval seconds is below your plan minimum of 60 seconds", details:{interval_seconds:[…]}}}PATCH … {codes:["NOPE_NOT_A_CODE"]}{status:"fail", data:{error:"VALIDATION_ERROR", message:"Codes contains invalid commodity codes: NOPE_NOT_A_CODE", details:{codes:[…]}}}PATCH … {status:"sleeping"}{error:{code:"INTERNAL_SERVER_ERROR", …}}(already tracked as OilpriceAPI/oilpriceapi-api#8471)POST …/resumestatus:"active",next_run_atre-armed to nowDELETE /v1/subscriptions/:idGETafter delete, unknown id{error:{code:"NOT_FOUND", message:"Subscription not found", request_id, docs}}GET /v1/subscriptions{status:"success", data:{subscriptions:[…]}}The raw probe created watch
c27641db-012d-4a22-8939-e77fe05d7eb4, paused it immediately, and deleted it infinally(DELETE 204, then GET 404). Fixtures intests/fixtures/subscriptions/are those bodies byte for byte.What changed
get(id),update(id, params),pause(id),resume(id), each returningSubscription.UpdateSubscriptionParams { name?, codes?, interval?, deliverWebhook?, status? }.intervaluses the same friendly mapping ascreate("1h"→interval_seconds: 3600).statusother thanactive/paused(the API 500s on it).ambiguousWrite: trueafter exactly one attempt.rawBody(upgrade_trigger,limit,upgrade.next_tier;details.<field>).list()no longer returns[]whensubscriptionsis missing. It raisesunexpected_response_shape;[]now means the API sent an empty list.create()and the new methods likewise raise on an envelope they cannot map instead of returning it as-is.Breaking changes (for the 2.0.0 notes)
subscriptions.list()throwsunexpected_response_shapeinstead of resolving[]for a body without asubscriptionsarray, and no longer accepts a bare array (production never sends one).subscriptions.create()throwsunexpected_response_shapeinstead of returning an unrecognised body typed asSubscription.No version bump in any of the four places.
Red first
tests/resources/subscriptions.test.tsnow drives the real client through afetchmock (the old suite spied on the privaterequest).tests/subscriptions-lifecycle.test-d.tsis new, in the typecheck project. Against unmodifiedsrc/:expected [] to be an instance of OilPriceAPIErroris the fabricated-defaultlist()failing its new test.Green
Full suite and checks on the branch (baseline on
main: 50 files, 703 passed / 1 skipped):Live lifecycle smoke: ran, passed
tests/live/subscriptions.test.tsadds create → pause (immediately) → get → update → resume → pause → delete infinally, then assertsgetraisesNotFoundError. Run against production with the test key:Afterwards the account held only one watch,
b84b24a0… mcp-live-contract-1788524538194(active). That watch was leaked by the mcp-server live contract on 2026-09-04 and is filed as OilpriceAPI/mcp-server#126. It was not touched here.Error messages in this PR's 404 test come from the canonical
{error:{…}}envelope, whichmainalready parses. The 422 test assertsrawBodyonly;messageandcodefor fail-envelope bodies arrive with #114 (#111). Both PRs are green in either merge order.Merge order
This PR, #113 (#108) and the #79 PR all add exports to
src/index.tsin different blocks. Git should merge them cleanly in any order; if one conflicts, it will be adjacent export lines only.🤖 Generated with Claude Code
https://claude.ai/code/session_015ao5paex73xXvuM424Libo