Skip to content
Open
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
95 changes: 95 additions & 0 deletions .claude/skills/pipedrive-v2-migrate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Pipedrive v2 Resource Migration

Migrate a Pipedrive resource from v1 to v2 in the pipedrive.rb gem.

**Usage:** `/pipedrive-v2-migrate <ResourceName>`
**Example:** `/pipedrive-v2-migrate deal`

The argument is the singular PascalCase class name (e.g. `Deal`, `Person`, `Pipeline`).

> **Resources NOT available in v2 (keep on v1):** Notes — stop and inform the user if they request one of these.
If no argument is given, ask the user which resource to migrate.

---

## Step 1 — Read the v1 resource

Read `lib/pipedrive/<resource_snake_case>.rb` to identify:
- Which CRUD modules are included (Create / Read / Update / Delete)
- Any custom methods beyond standard CRUD (e.g. `find_by_name` on Person/Organization)

Custom methods will need a v2 equivalent — flag them to the user rather than silently
dropping them.

## Step 2 — Create `lib/pipedrive/v2/<resource>.rb`

Mirror the pattern of [lib/pipedrive/v2/activity.rb](lib/pipedrive/v2/activity.rb).
Include exactly the same operations that the v1 class has.

```ruby
# frozen_string_literal: true

module Pipedrive
module V2
class Deal < ::Pipedrive::V2::Base
include ::Pipedrive::V2::Operations::Create
include ::Pipedrive::V2::Operations::Read
include ::Pipedrive::V2::Operations::Update
include ::Pipedrive::V2::Operations::Delete
end
end
end
```

## Step 3 — Create `spec/lib/pipedrive/v2/<resource>_spec.rb`

Mirror the pattern of [spec/lib/pipedrive/v2/activity_spec.rb](spec/lib/pipedrive/v2/activity_spec.rb).

The spec **must** cover these cases:

1. **`#entity_name`** — returns the pluralized snake_case resource name (e.g. `"notes"`)
2. **`#build_url`** — returns `/api/v2/<plural>` with no args
3. **`#build_url` with id** — returns `/api/v2/<plural>/42`
4. **`#build_url` no token** — does NOT include `api_token` as query param
5. **`#connection`** — `x-api-token` header equals the token passed to `.new`
6. **`#create`** — stubs POST to `/api/v2/<plural>`, asserts `x-api-token` header, asserts `be_success`
7. **`#find_by_id`** — stubs GET `/api/v2/<plural>/1`, asserts `x-api-token` header, asserts `be_success`
8. **`#update`** — stubs **PATCH** (not PUT) to `/api/v2/<plural>/1`, asserts `x-api-token` header, asserts `be_success`
9. **`#delete`** — stubs DELETE to `/api/v2/<plural>/1`, asserts `x-api-token` header, asserts `be_success`
10. **`#all` cursor pagination** — stubs two pages (first returns `next_cursor: 'abc'`, second returns `next_cursor: nil`), asserts all items are collected

Use `stub_request` with `headers: { 'x-api-token' => 'token' }` on every stub.
Response bodies must be valid JSON with `success: true` and `data: {}` (or `data: []` for list endpoints).

## Step 4 — Register the require in `lib/pipedrive.rb`

Find the existing `require 'pipedrive/<resource>'` line and add the v2 require immediately after:

```ruby
# Deals
require 'pipedrive/deal'
require 'pipedrive/v2/deal' # add this line
```

## Step 5 — Update `HISTORY.md` and `lib/pipedrive/version.rb`

Bump the patch version in `lib/pipedrive/version.rb` and add an entry at the top of `HISTORY.md`:

```markdown
## [v0.X.Y](https://github.com/hostnfly/pipedrive.rb/compare/v0.X.(Y-1)...v0.X.Y) - YYYY-MM-DD

### Added
- `Pipedrive::V2::<Resource>` — <Resource> endpoint migrated to API v2
```

## Step 6 — Run the specs

```bash
bundle exec rspec spec/lib/pipedrive/v2/<resource>_spec.rb
```

Fix any failures before declaring done. Then run the full suite to check for regressions:

```bash
bundle exec rspec
```
22 changes: 22 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [v0.4.3](https://github.com/hostnfly/pipedrive.rb/compare/v0.4.2...v0.4.3) - 2026-06-18

### Added
- `Pipedrive::V2::Deal` — Deals endpoint migrated to API v2

## [v0.4.2](https://github.com/hostnfly/pipedrive.rb/compare/v0.4.1...v0.4.2) - 2026-02-27

### Fixed
- JSON request body encoding for v2 endpoints

## [v0.4.1](https://github.com/hostnfly/pipedrive.rb/compare/v0.4.0...v0.4.1) - 2026-02-27

### Added
- `Pipedrive::V2::Activity` — Activities endpoint migrated to API v2

## [v0.4.0](https://github.com/hostnfly/pipedrive.rb/compare/v0.3.0...v0.4.0) - 2026-02-27

### Added
- Pipedrive API v2 infrastructure (`Pipedrive::V2::Base`, `V2::Utils`, `V2::Operations::*`)
- Cursor-based pagination for v2 endpoints
- `x-api-token` header authentication for v2

## [v0.3.0](https://github.com/amoniacou/pipedrive.rb/compare/v0.2.0...v0.3.0) - 2020-10-10

### Merged
Expand Down
11 changes: 11 additions & 0 deletions lib/pipedrive.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

require 'logger'
require 'active_support/core_ext/hash'
require 'active_support/core_ext/array'
require 'active_support/concern'
require 'active_support/inflector'

Expand Down Expand Up @@ -51,6 +52,14 @@ def logger
require 'pipedrive/operations/update'
require 'pipedrive/operations/delete'

# Core V2
require 'pipedrive/v2/utils'
require 'pipedrive/v2/base'
require 'pipedrive/v2/operations/create'
require 'pipedrive/v2/operations/read'
require 'pipedrive/v2/operations/update'
require 'pipedrive/v2/operations/delete'

# Persons
require 'pipedrive/person_field'
require 'pipedrive/person'
Expand Down Expand Up @@ -78,10 +87,12 @@ def logger
# Activities
require 'pipedrive/activity'
require 'pipedrive/activity_type'
require 'pipedrive/v2/activity'

# Deals
require 'pipedrive/deal_field'
require 'pipedrive/deal'
require 'pipedrive/v2/deal'

# Files
require 'pipedrive/file'
Expand Down
38 changes: 28 additions & 10 deletions lib/pipedrive/base.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# frozen_string_literal: true

module Pipedrive
class RateLimitError < StandardError
attr_reader :response

def initialize(response)
@response = response
end
end

class Base
def initialize(api_token = ::Pipedrive.api_token)
raise 'api_token should be set' unless api_token.present?
Expand All @@ -17,16 +25,13 @@ def make_api_call(*args)
method = args[0]
raise 'method param missing' unless method.present?

url = build_url(args, params.delete(:fields_to_select))
begin
url = build_url(args, params.delete(:fields_to_select))
res = connection.__send__(method.to_sym, url, params)
rescue Errno::ETIMEDOUT
retry
rescue Faraday::ParsingError
sleep 5
retry
rescue Faraday::ParsingError => e
res = e.response
end
process_response(res)
process_response(res).merge(status: res.status)
end

def build_url(args, fields_to_select = nil)
Expand All @@ -50,13 +55,20 @@ def process_response(res)
end

def failed_response(res)
failed_res = res.body.merge(success: false, not_authorized: false,
failed: false)
failed_res = if res.body.is_a?(::Hashie::Mash)
res.body.merge(success: false, not_authorized: false, failed: false, not_found: false)
else
::Hashie::Mash.new(success: false, not_authorized: false, failed: false, not_found: false)
end
case res.status
when 401
failed_res[:not_authorized] = true
when 404
failed_res[:not_found] = true
when 420
failed_res[:failed] = true
when 429
raise RateLimitError.new(res)
end
failed_res
end
Expand All @@ -82,14 +94,20 @@ def faraday_options
# :nodoc
def connection
@connection ||= Faraday.new(faraday_options) do |conn|
conn.request :url_encoded
conn.request params_format
conn.response :mashify
conn.response :json, content_type: /\bjson$/
conn.use FaradayMiddleware::ParseJson
conn.response :logger, ::Pipedrive.logger if ::Pipedrive.debug
conn.adapter Faraday.default_adapter
end
end

private

def params_format
:url_encoded
end
end
end
end
12 changes: 12 additions & 0 deletions lib/pipedrive/v2/activity.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module Pipedrive
module V2
class Activity < ::Pipedrive::V2::Base
include ::Pipedrive::V2::Operations::Create
include ::Pipedrive::V2::Operations::Read
include ::Pipedrive::V2::Operations::Update
include ::Pipedrive::V2::Operations::Delete
end
end
end
28 changes: 28 additions & 0 deletions lib/pipedrive/v2/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# frozen_string_literal: true

module Pipedrive
module V2
class Base < ::Pipedrive::Base
class << self
private

# V2 API requires JSON request bodies to preserve integer types.
def params_format
:json
end
end

def build_url(args, _fields_to_select = nil)
url = +"/api/v2/#{entity_name}"
url << "/#{args[1]}" if args[1]
url
end

def connection
conn = self.class.connection.dup
conn.headers['x-api-token'] = @api_token
conn
end
end
end
end
12 changes: 12 additions & 0 deletions lib/pipedrive/v2/deal.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module Pipedrive
module V2
class Deal < ::Pipedrive::V2::Base
include ::Pipedrive::V2::Operations::Create
include ::Pipedrive::V2::Operations::Read
include ::Pipedrive::V2::Operations::Update
include ::Pipedrive::V2::Operations::Delete
end
end
end
15 changes: 15 additions & 0 deletions lib/pipedrive/v2/operations/create.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# frozen_string_literal: true

module Pipedrive
module V2
module Operations
module Create
extend ActiveSupport::Concern

def create(params)
make_api_call(:post, params)
end
end
end
end
end
19 changes: 19 additions & 0 deletions lib/pipedrive/v2/operations/delete.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

module Pipedrive
module V2
module Operations
module Delete
extend ActiveSupport::Concern

def delete(id)
make_api_call(:delete, id)
end

def delete_all
make_api_call(:delete)
end
end
end
end
end
34 changes: 34 additions & 0 deletions lib/pipedrive/v2/operations/read.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

module Pipedrive
module V2
module Operations
module Read
extend ActiveSupport::Concern
include ::Enumerable
include ::Pipedrive::V2::Utils

def each(params = {}, &block)
return to_enum(:each, params) unless block_given?

follow_pagination(:chunk, [], params, &block)
end

def all(params = {})
each(params).to_a
end

def chunk(params = {})
res = make_api_call(:get, params)
return [] unless res.success?

res
end

def find_by_id(id)
make_api_call(:get, id)
end
end
end
end
end
20 changes: 20 additions & 0 deletions lib/pipedrive/v2/operations/update.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# frozen_string_literal: true

module Pipedrive
module V2
module Operations
module Update
extend ActiveSupport::Concern

def update(*args)
params = args.extract_options!
params.symbolize_keys!
id = params.delete(:id) || args[0]
raise 'id must be provided' unless id

make_api_call(:patch, id, params)
end
end
end
end
end
Loading