Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions .beads/issues.jsonl
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
{"id":"nb-0a4","title":"Add update method to People resource","description":"Implement People.update() method to update person attributes via PATCH /api/v2/signups/:id with JSON:API format. Includes unit tests for success/error cases and address updates. See SPEC_ADD_PEOPLE_UPDATE.md","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-10T13:12:40.463653+07:00","updated_at":"2025-12-10T13:17:20.533506+07:00","closed_at":"2025-12-10T13:17:20.533506+07:00","dependencies":[{"issue_id":"nb-0a4","depends_on_id":"nb-5","type":"blocks","created_at":"2025-12-10T13:12:45.059752+07:00","created_by":"daemon"}]}
{"id":"nb-1","title":"Events Resource","description":"Create Events resource with list, create, show, update, delete operations including RSVP management and event-specific filtering capabilities","design":"Create lib/nationbuilder_api/resources/events.rb with RSVP support. See roadmap item #16.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-02T17:09:35.786297+07:00","updated_at":"2025-12-02T17:26:17.182348+07:00","dependencies":[{"issue_id":"nb-1","depends_on_id":"nb-5","type":"blocks","created_at":"2025-12-02T17:12:15.193639+07:00","created_by":"daemon"}]}
{"id":"nb-1","title":"Events Resource","description":"Create Events resource with list, create, show, update, delete operations including RSVP management and event-specific filtering capabilities","design":"Create lib/nationbuilder_api/resources/events.rb with RSVP support. See roadmap item #16.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-02T17:09:35.786297+07:00","updated_at":"2026-01-29T20:44:31.902623+07:00","closed_at":"2026-01-29T20:44:31.902623+07:00","dependencies":[{"issue_id":"nb-1","depends_on_id":"nb-5","type":"blocks","created_at":"2025-12-02T17:12:15.193639+07:00","created_by":"daemon"}]}
{"id":"nb-10","title":"Production Memory Adapter Warning","description":"Add runtime warning when Memory adapter is used in production Rails environments to prevent accidental production usage","design":"Add check in Memory adapter initialization that logs warning if Rails.env.production?. See roadmap item #11.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-02T17:09:13.087752+07:00","updated_at":"2025-12-02T17:26:17.186615+07:00"}
{"id":"nb-11","title":"Donations Resource","description":"Build Donations endpoint support with list/filter/pagination, create, show, update operations and proper handling of money values, dates, and donor relationships","design":"Create lib/nationbuilder_api/resources/donations.rb. Handle money formatting, date parsing. See roadmap item #15.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-02T17:09:34.642315+07:00","updated_at":"2025-12-02T17:26:17.186862+07:00","dependencies":[{"issue_id":"nb-11","depends_on_id":"nb-5","type":"blocks","created_at":"2025-12-02T17:12:13.279819+07:00","created_by":"daemon"}]}
{"id":"nb-11","title":"Donations Resource","description":"Build Donations endpoint support with list/filter/pagination, create, show, update operations and proper handling of money values, dates, and donor relationships","design":"Create lib/nationbuilder_api/resources/donations.rb. Handle money formatting, date parsing. See roadmap item #15.","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-02T17:09:34.642315+07:00","updated_at":"2026-01-29T20:42:17.201894+07:00","closed_at":"2026-01-29T20:42:17.201894+07:00","dependencies":[{"issue_id":"nb-11","depends_on_id":"nb-5","type":"blocks","created_at":"2025-12-02T17:12:13.279819+07:00","created_by":"daemon"}]}
{"id":"nb-12","title":"Add Explicit Linter Configuration","description":"Create .standard.yml file for explicit StandardRB configuration and team consistency beyond defaults","design":"Add .standard.yml with project-specific linter rules. See roadmap item #12.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-02T17:09:14.308639+07:00","updated_at":"2025-12-02T17:26:17.187136+07:00"}
{"id":"nb-13","title":"Enhance HTTP Error Context","description":"Add request method and path to error messages for improved debugging (e.g., 'Authentication failed for GET /people')","design":"Update error raising in http_client.rb handle_response method to include method and path context. See roadmap item #8.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-02T17:09:08.185816+07:00","updated_at":"2025-12-04T10:16:52.99755+07:00","closed_at":"2025-12-04T10:16:52.99755+07:00"}
{"id":"nb-14","title":"Centralize OAuth HTTP Calls","description":"Refactor OAuth module to use HttpClient instead of direct HTTP.post calls for consistent logging and error handling across all HTTP operations","design":"Replace Net::HTTP direct calls in oauth.rb with HttpClient calls. Ensure token exchange and refresh operations use centralized client. See roadmap item #9.","status":"closed","priority":2,"issue_type":"task","created_at":"2025-12-02T17:09:09.633647+07:00","updated_at":"2025-12-04T10:25:59.122885+07:00","closed_at":"2025-12-04T10:25:59.122885+07:00"}
{"id":"nb-2","title":"Add Network Error Test Coverage","description":"Implement tests for timeout scenarios and network error handling to ensure resilience","design":"Add tests for Net::OpenTimeout, Net::ReadTimeout, connection refused scenarios. See roadmap item #13.","status":"open","priority":3,"issue_type":"task","created_at":"2025-12-02T17:09:16.462163+07:00","updated_at":"2025-12-02T17:26:17.183337+07:00"}
{"id":"nb-2o7","title":"Remove ENV var requirement for NationBuilder credentials","description":"Update initializer to not set global credentials, remove ENV vars from test_helper. Credentials should only be passed via Client.new parameters.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-10T18:48:01.949364+07:00","updated_at":"2025-12-10T18:51:02.016399+07:00","closed_at":"2025-12-10T18:51:02.016399+07:00","external_ref":"https://github.com/ebrett/nationbuilder-client-v2/issues/7"}
{"id":"nb-3","title":"Add Redis \u0026 ActiveRecord Adapter Tests","description":"Implement comprehensive test suites for Redis and ActiveRecord token storage adapters to match Memory adapter test coverage. This addresses 3 failing ActiveRecord tests found in verification.","design":"Add test files spec/nationbuilder_api/token_storage/redis_spec.rb and enhance active_record_spec.rb. Include store_token, retrieve_token, delete_token, serialization tests. See roadmap item #7.","status":"closed","priority":1,"issue_type":"task","created_at":"2025-12-02T17:09:06.202895+07:00","updated_at":"2025-12-02T17:32:40.986334+07:00","closed_at":"2025-12-02T17:32:40.986334+07:00"}
{"id":"nb-4","title":"Fix Token Adapter Validation","description":"Correct validate_adapter_interface! method in client.rb to use respond_to? instead of instance_methods set difference for accurate interface validation","design":"Change from instance_methods set comparison to respond_to? checks for each required method (get_token, store_token, delete_token). See roadmap item #6.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-02T17:09:04.540968+07:00","updated_at":"2025-12-02T17:26:17.184025+07:00","closed_at":"2025-12-02T17:18:07.625684+07:00"}
{"id":"nb-5","title":"Response Object Pattern","description":"Wrap API responses in typed objects instead of raw hashes for better developer experience and type safety (e.g., Person, Donation objects)","design":"Create response object classes in lib/nationbuilder_api/response_objects/. Use method_missing or define_method for attribute access. See roadmap item #18.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-02T17:09:38.473982+07:00","updated_at":"2025-12-02T17:26:17.184279+07:00"}
{"id":"nb-6","title":"Tags Resource","description":"Implement tag management with list tags, apply tag to person, remove tag from person, and bulk tagging operations for managing supporter segments","design":"Create lib/nationbuilder_api/resources/tags.rb. Support bulk operations. See roadmap item #17.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-02T17:09:36.894093+07:00","updated_at":"2025-12-02T17:26:17.185491+07:00","dependencies":[{"issue_id":"nb-6","depends_on_id":"nb-5","type":"blocks","created_at":"2025-12-02T17:12:17.88942+07:00","created_by":"daemon"}]}
{"id":"nb-7","title":"People Resource - Full CRUD","description":"Implement full CRUD operations for People endpoint including list with filtering/pagination, create, show, update, delete, and search functionality with proper parameter handling","design":"Create lib/nationbuilder_api/resources/people.rb with methods: list, create, show, update, delete, search. Follow REST conventions. See roadmap item #14.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-02T17:09:33.096076+07:00","updated_at":"2025-12-02T17:26:17.185816+07:00","dependencies":[{"issue_id":"nb-7","depends_on_id":"nb-5","type":"blocks","created_at":"2025-12-02T17:12:11.989747+07:00","created_by":"daemon"}]}
{"id":"nb-5","title":"Response Object Pattern","description":"Wrap API responses in typed objects instead of raw hashes for better developer experience and type safety (e.g., Person, Donation objects)","design":"Create response object classes in lib/nationbuilder_api/response_objects/. Use method_missing or define_method for attribute access. See roadmap item #18.","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-02T17:09:38.473982+07:00","updated_at":"2026-01-29T20:31:08.815565+07:00","closed_at":"2026-01-29T20:31:08.815565+07:00"}
{"id":"nb-6","title":"Tags Resource","description":"Implement tag management with list tags, apply tag to person, remove tag from person, and bulk tagging operations for managing supporter segments","design":"Create lib/nationbuilder_api/resources/tags.rb. Support bulk operations. See roadmap item #17.","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-02T17:09:36.894093+07:00","updated_at":"2026-01-29T20:39:31.50818+07:00","closed_at":"2026-01-29T20:39:31.50818+07:00","dependencies":[{"issue_id":"nb-6","depends_on_id":"nb-5","type":"blocks","created_at":"2025-12-02T17:12:17.88942+07:00","created_by":"daemon"}]}
{"id":"nb-7","title":"People Resource - Full CRUD","description":"Implement full CRUD operations for People endpoint including list with filtering/pagination, create, show, update, delete, and search functionality with proper parameter handling","design":"Create lib/nationbuilder_api/resources/people.rb with methods: list, create, show, update, delete, search. Follow REST conventions. See roadmap item #14.","status":"closed","priority":2,"issue_type":"feature","assignee":"claude","created_at":"2025-12-02T17:09:33.096076+07:00","updated_at":"2026-01-29T20:37:58.899562+07:00","closed_at":"2026-01-29T20:37:58.899562+07:00","dependencies":[{"issue_id":"nb-7","depends_on_id":"nb-5","type":"blocks","created_at":"2025-12-02T17:12:11.989747+07:00","created_by":"daemon"}]}
{"id":"nb-8","title":"Fix OAuth VCR Test Failures","description":"Fix 5 failing OAuth integration tests due to VCR cassette configuration issues. Tests in oauth_flow_spec.rb and client_spec.rb fail with 'VCR does not know how to handle' errors.","design":"Review VCR cassette configuration, regenerate cassettes if needed, ensure Net::HTTP compatibility. See verification report from 2025-11-25-switch-to-net-http spec.","status":"closed","priority":1,"issue_type":"bug","created_at":"2025-12-02T17:09:18.00717+07:00","updated_at":"2025-12-02T17:26:17.186112+07:00","closed_at":"2025-12-02T17:21:53.298386+07:00"}
{"id":"nb-9","title":"Add Rate Limit Monitoring","description":"Log rate limit headers (X-RateLimit-Remaining, X-RateLimit-Reset) for proactive rate limit monitoring and debugging","design":"Add rate limit header logging in http_client.rb after response received. Log at info level. See roadmap item #10.","status":"open","priority":2,"issue_type":"task","created_at":"2025-12-02T17:09:10.928534+07:00","updated_at":"2025-12-02T17:26:17.186369+07:00"}
{"id":"nb-f0q","title":"V1 API support for people taggings and tags","description":"Add V1 API methods for taggings and tags because V2 API doesn't return tag names.\n\nRequired methods:\n- client.people.taggings(person_id) - GET /api/v1/people/:person_id/taggings\n- client.people.add_tagging(person_id, tag_name) - PUT /api/v1/people/:person_id/taggings\n- client.people.remove_tagging(person_id, tag_name) - DELETE /api/v1/people/:person_id/taggings/:tag\n- client.tags.list - GET /api/v1/tags\n\nV1 API uses plain JSON format (not JSON:API), so response handling will differ from V2 endpoints.\n\nThis will allow Citizen app to replace hardcoded API calls with proper gem methods.","design":"Create or extend lib/nationbuilder_api/resources/people.rb to add V1 tagging methods. Create lib/nationbuilder_api/resources/tags.rb for V1 tags list endpoint. Handle plain JSON responses (not JSON:API format). Add integration tests verifying V1 endpoints work correctly.","status":"in_progress","priority":1,"issue_type":"feature","assignee":"claude","created_at":"2026-01-29T11:44:00.390272+07:00","updated_at":"2026-01-29T11:48:37.805016+07:00","external_ref":"https://github.com/ebrett/nationbuilder-client-v2/issues/11"}
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,40 @@ client = NationbuilderApi::Client.new(

**Important**: Credentials (`client_id`, `client_secret`, `redirect_uri`) are **not required** in global configuration. You can pass them per-instance, which is especially useful for multi-tenant applications.

### Response Objects (Optional)

By default, API responses are returned as raw hashes. You can optionally enable response objects for a better developer experience with typed objects and convenient attribute access:

```ruby
# Enable response objects globally
NationbuilderApi.configure do |config|
config.wrap_responses = true
end

# Or per-client instance
client = NationbuilderApi::Client.new(
# ... credentials ...
wrap_responses: true
)

# With response objects enabled
person = client.people.show(123)
person.first_name # => "John" (method access)
person.full_name # => "John Doe" (computed attribute)
person[:data] # => {...} (still supports hash access for backward compatibility)
person.to_h # => original hash

# Works with both V1 and V2 API responses
```

**Benefits of response objects:**
- Convenient method access (`person.first_name` instead of `person[:data][:attributes][:first_name]`)
- Computed attributes (`person.full_name`)
- Backward compatible (still supports hash access via `[]`, `dig`, etc.)
- Type safety and better IDE autocomplete

**Note**: Response objects default to `false` for backward compatibility. Enable when you're ready to migrate.

## Multi-Tenant Usage

The gem is designed to support multi-tenant applications where you manage multiple NationBuilder accounts, each with their own OAuth credentials. This is the recommended pattern for SaaS applications.
Expand Down
8 changes: 8 additions & 0 deletions lib/nationbuilder_api.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ module Resources
autoload :Base, "nationbuilder_api/resources/base"
autoload :People, "nationbuilder_api/resources/people"
autoload :Tags, "nationbuilder_api/resources/tags"
autoload :Donations, "nationbuilder_api/resources/donations"
autoload :Events, "nationbuilder_api/resources/events"
end

# Response objects
module ResponseObjects
autoload :Base, "nationbuilder_api/response_objects/base"
autoload :Person, "nationbuilder_api/response_objects/person"
end

# OAuth scope constants
Expand Down
23 changes: 23 additions & 0 deletions lib/nationbuilder_api/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,29 @@ def tags
@tags ||= Resources::Tags.new(self)
end

# Access Donations resource
#
# @return [Resources::Donations] Donations resource instance
#
# @example
# client.donations.list
# client.donations.show(123)
def donations
@donations ||= Resources::Donations.new(self)
end

# Access Events resource
#
# @return [Resources::Events] Events resource instance
#
# @example
# client.events.list
# client.events.show(123)
# client.events.rsvps(123)
def events
@events ||= Resources::Events.new(self)
end

private

# Extract OAuth base URL from API base URL
Expand Down
4 changes: 3 additions & 1 deletion lib/nationbuilder_api/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,21 @@
module NationbuilderApi
class Configuration
attr_accessor :client_id, :client_secret, :redirect_uri, :base_url,
:log_level, :timeout, :ssl_verify
:log_level, :timeout, :ssl_verify, :wrap_responses
attr_writer :logger, :token_adapter

DEFAULT_BASE_URL = "https://api.nationbuilder.com/v2"
DEFAULT_TIMEOUT = 30
DEFAULT_LOG_LEVEL = :info
DEFAULT_SSL_VERIFY = true
DEFAULT_WRAP_RESPONSES = false

def initialize
@base_url = DEFAULT_BASE_URL
@timeout = DEFAULT_TIMEOUT
@log_level = DEFAULT_LOG_LEVEL
@ssl_verify = DEFAULT_SSL_VERIFY
@wrap_responses = DEFAULT_WRAP_RESPONSES
@logger = nil
@token_adapter = nil
@client_id = nil
Expand Down
95 changes: 95 additions & 0 deletions lib/nationbuilder_api/resources/donations.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# frozen_string_literal: true

module NationbuilderApi
module Resources
# Donations API resource
# Provides access to NationBuilder Donations endpoints for managing donations
class Donations < Base
# List all donations
# Uses V2 API with JSON:API format
#
# @param page [Integer, nil] Page number for pagination
# @param per_page [Integer, nil] Number of results per page
# @param filter [Hash, nil] Filter parameters (e.g., donor_id)
# @return [Hash] List of donations in JSON:API format
#
# @example
# client.donations.list
# # => { data: [{type: "donation", id: "1", ...}, ...] }
#
# @example With pagination
# client.donations.list(page: 2, per_page: 50)
#
# @example With filtering
# client.donations.list(filter: {donor_id: "123"})
def list(page: nil, per_page: nil, filter: nil)
params = {}
params[:page] = {number: page, size: per_page} if page || per_page
params[:filter] = filter if filter

get("/api/v2/donations", params: params)
end

# Fetch a single donation by ID
# Uses V2 API with JSON:API format
#
# @param id [String, Integer] Donation ID
# @return [Hash] Donation data in JSON:API format
#
# @example
# client.donations.show(123)
# # => { data: { type: "donation", id: "123", attributes: { ... } } }
def show(id)
get("/api/v2/donations/#{id}")
end

# Create a new donation
# Uses V2 API with JSON:API format
#
# @param attributes [Hash] Donation attributes (amount_in_cents, donor_id, donated_at, etc.)
# @return [Hash] Created donation data in JSON:API format
# @raise [ValidationError] If attributes are invalid
#
# @example
# client.donations.create(attributes: {
# amount_in_cents: 5000,
# donor_id: "123",
# donated_at: "2025-01-15T10:00:00Z"
# })
def create(attributes:)
body = {
data: {
type: "donations",
attributes: attributes
}
}
post("/api/v2/donations", body: body)
end

# Update a donation
# Uses V2 API with JSON:API format
#
# @param id [String, Integer] Donation ID
# @param attributes [Hash] Donation attributes to update
# @return [Hash] Updated donation data in JSON:API format
# @raise [ValidationError] If attributes are invalid
# @raise [NotFoundError] If donation not found
#
# @example
# client.donations.update(123, attributes: {
# amount_in_cents: 7500,
# note: "Updated donation amount"
# })
def update(id, attributes:)
body = {
data: {
type: "donations",
id: id.to_s,
attributes: attributes
}
}
patch("/api/v2/donations/#{id}", body: body)
end
end
end
end
Loading