Patch Retention API Wrapper

CI Gem Version

Installation

Install the gem and add to the application's Gemfile by executing:

$ bundle add patch_retention

If bundler is not being used to manage dependencies, install the gem by executing:

$ gem install patch_retention

Usage

Configuration

This gem uses dotenv for managing environment variables in development and test environments. Copy the example file and add your credentials:

# For development
cp .env.example .env.development.local
# Edit .env.development.local with your credentials

# For running tests
cp .env.test.example .env.test
# Edit .env.test with your test credentials

The following files are used:

  • .env.test - Test environment (gitignored - copy from .env.test.example)
  • .env.test.example - Test environment template (committed)
  • .env.development - Development defaults (committed)
  • .env.development.local - Your local development credentials (gitignored)
  • .env.example - Example configuration (committed)

Using Environment Variables

For production or CI environments, you can set the following environment variables:

PATCH_RETENTION_API_URL='your_custom_api_url' # Optional, by default is set to: https://api.patchretention.com/v2
PATCH_RETENTION_CLIENT_ID='your_client_id'
PATCH_RETENTION_CLIENT_SECRET='your_client_secret'
PATCH_RETENTION_PROXY_URL='your_proxy_url' # Optional, for routing requests through a proxy

Credentials might also be set in the initializer:

PatchRetention.configure do |config|
    config.api_url = 'your_custom_api_url' # Optional, by default is set to: https://api.patchretention.com/v2
    config.client_id = 'your_client_id'
    config.client_secret = 'your_client_secret'
    config.proxy_url = 'your_proxy_url' # Optional, for routing requests through a proxy
end

Using a Custom Configuration per Call

While you can configure the gem globally, there might be scenarios where you need to use a different set of credentials or API endpoint for a specific API call. All API call methods (e.g., PatchRetention::Products.create, PatchRetention::Memberships.create, PatchRetention::Events.create, PatchRetention::Contacts.find_or_create_by, etc.) accept an optional config: parameter.

This parameter expects an instance of PatchRetention::Configuration.

Example: Creating a call with a custom PatchRetention::Configuration Object:

If you want to use different configuration values for a specific call, you can create a new PatchRetention::Configuration object, populate it, and pass it to the desired method.

# Create a custom configuration instance
custom_config = PatchRetention::Configuration.new
custom_config.client_id = "your_other_client_id"
custom_config.client_secret = "your_other_client_secret"
custom_config.api_url = "https://specific-instance.patchretention.com/v2"
# custom_config.proxy_url = "http://your.other.proxy.com:8080" # Optional

# Use this custom configuration for an API call
begin
  product_details = {
    name: "Special Product",
    price: 1500,
    status: "PUBLISHED"
  }
  new_product = PatchRetention::Products.create(**product_details, config: custom_config)
  puts "Created product with custom config: #{new_product}"

  membership_payload = {
    contact_id: "ct_somecontact",
    product_id: new_product["id"], # Assuming new_product is a hash with an "id" key
    start_at: Time.now.xmlschema
  }
  new_membership = PatchRetention::Memberships.create(**membership_payload, config: custom_config)
  puts "Created membership with custom config: #{new_membership}"

rescue PatchRetention::Error => e
  puts "Error with custom config: #{e.message}"
end

This approach allows you to maintain multiple configurations if needed, for example, when interacting with different Patch Retention accounts or environments within the same application. If you need to use a highly customized Faraday connection (e.g., with special middleware), you would need to ensure the PatchRetention.connection method can accommodate this through the Configuration object, potentially by enhancing the Configuration class or the PatchRetention.connection method itself to interpret more advanced settings from the Configuration object.

Usage

First, you need to configure the gem with your API credentials globally (if not providing custom configurations per call).

Then, you can use the gem's methods to interact with the API.

Contacts:

To retrieve all contacts:

# Basic usage
contacts = PatchRetention::Contacts.all

# With pagination (using keyword arguments)
contacts = PatchRetention::Contacts.all(limit: 10, offset: 0)

# With email filter
contacts = PatchRetention::Contacts.all(email: '[email protected]')

# With pagination and email filter
contacts = PatchRetention::Contacts.all(
  limit: 50,
  offset: 0,
  email: '[email protected]'
)

To retrieve a single contact:

contact = PatchRetention::Contacts.find(1)

To create a contact:

contact = PatchRetention::Contacts.find_or_create_by(
  contact_params: {
    first_name: 'John',
    last_name: 'Doe',
    email: '[email protected]',
    phone: '1234567890',
    address: '123 Main St',
    city: 'New York',
    state: 'NY',
    zip: '10001',
    country: 'US',
    company: 'PlayByPoint',
    job_title: 'Developer'
  },
  query_params: {
    email: '[email protected]',
    phone: '1234567890'
  }
)

To update a contact:

contact = PatchRetention::Contacts.update(1, {
  first_name: 'John',
  last_name: 'Doe',
  email: '[email protected]',
  phone: '1234567890',
  sms_on: true,
  email_on: true
})

To delete a contact:

PatchRetention::Contacts.delete(1)

Events

To retrieve all events:

events = PatchRetention::Events.all

To retrieve a single event:

event = PatchRetention::Events.find(1)

To create an event:

event = PatchRetention::Events.create(
  event_type: 'order.finished',
  primary_key_details: {
    key: 'email',
    value: '[email protected]'
  },
  data: {
    "external_id": "1234567890",
    "subtotal_amount": 3000,
    "total_tax": 300,
    "total_amount": 3300,
    "total_discounts": 0,
    "facility_id": 1,
  },
  contact_details: {
    upsert: true,
    params: {
      # Optional, if upsert is true
      first_name: 'John',
      last_name: 'Doe',
      email: '[email protected]',
      phone: '1234567890',
      address: '123 Main St',
      city: 'New York'
    }
  },
  at: Time.now,
)

Products

To create a product:

product = PatchRetention::Products.create(
  name: "Test Product",
  price: 999, # Price in Cents
  status: "PUBLISHED", # or "UNPUBLISHED"
  description: "This is a product for testing purposes.", # Optional
  membership: true, # Optional, boolean
  tags: ["test", "new_product"], # Optional, array of strings
  external_id: "SKU12345", # Optional
  external_data: { "custom_key": "custom_value" } # Optional, hash
)
# => {"id"=>"prod_xxxxxxxxxxxxxx", "name"=>"Test Product", ...}

To update a product:

product = PatchRetention::Products.update(
  product_id: "prod_xxxxxxxxxxxxxx",
  name: "Updated Product Name",
  price: 1299,
  status: "UNPUBLISHED"
)

To find a product:

product = PatchRetention::Products.find(product_id: "prod_xxxxxxxxxxxxxx")

Memberships

To create a membership:

require 'time'

membership = PatchRetention::Memberships.create(
  contact_id: "68273ecd9xxxxxxxxxxxx", # Required, ID of the contact
  product_id: "65de5xxxxxxxxxxxxx", # Required, ID of the product (e.g., from product creation)
  start_at: Time.now.xmlschema, # Optional, ISO8601 timestamp
  end_at: (Time.now + 30*24*60*60).xmlschema, # Optional, ISO8601 timestamp (e.g., 30 days from now)
  next_billing_at: (Time.now + 7*24*60*60).xmlschema, # Optional, ISO8601 timestamp for next billing date
  external_id: "MEM123", # Optional
  data: {
    "is_trial" => true,
    "trial_ends_at" => (Time.now + 15*24*60*60).xmlschema
  }, # Optional, hash for custom data
  tags: ["trial", "api_created"] # Optional, array of strings
)
# => {"id"=>"mem_xxxxxxxxxxxxxx", "contact_id"=>"ct_xxxxxxxxxxxxxx", ...}

To update a membership:

membership = PatchRetention::Memberships.update(
  membership_id: "mem_xxxxxxxxxxxxxx",
  end_at: (Time.now + 60*24*60*60).xmlschema, # Extend to 60 days
  tags: ["extended", "premium"]
)

To find a membership:

membership = PatchRetention::Memberships.find(membership_id: "mem_xxxxxxxxxxxxxx")

Calendar Items

To create a calendar item:

calendar_item = PatchRetention::CalendarItems.create(
  contact_id: "ct_xxxxxxxxxxxxxx", # Required, ID of the contact
  title: "Tennis Court Reservation", # Required, title of the event
  start_at: "2025-02-01T10:00:00Z", # Required, ISO8601 timestamp - when the event starts
  end_at: "2025-02-01T11:00:00Z", # Required, ISO8601 timestamp - when the event ends
  run_start_at: "2025-02-01T09:30:00Z", # Optional, ISO8601 - when to run the event for this item (defaults to start_at if not set)
  run_end_at: "2025-02-01T11:30:00Z", # Optional, ISO8601 - when to run the event for this item (defaults to end_at if not set)
  data: { # Optional, additional metadata that will be included in the calendar item
    external_id: "res_123456", # Your system's ID
    facility_id: "fac_789",
    court_name: "Court 1",
    reservation_type: "match"
  },
  tags: ["tennis", "premium", "court1"], # Optional, array of tags to categorize the calendar item
  time_occurred: "2025-01-28T09:00:00Z", # Optional, ISO8601 - past or current time for the event (will default to now)
  skip_triggers: false # Optional, if set to false or empty, any automation tied to the calendar item will not run
)
# => {"id"=>"cal_xxxxxxxxxxxxxx", "contact_id"=>"ct_xxxxxxxxxxxxxx", ...}

To update a calendar item:

calendar_item = PatchRetention::CalendarItems.update(
  calendar_item_id: "cal_xxxxxxxxxxxxxx",
  title: "Updated Tennis Match",
  end_at: "2025-02-01T12:00:00Z", # Extend by 1 hour
  data: {
    description: "Match extended due to tie-break"
  },
  tags: ["Tennis", "Extended", "Tie-break"] # Tags as separate parameter
)

To find a calendar item:

calendar_item = PatchRetention::CalendarItems.find(calendar_item_id: "cal_xxxxxxxxxxxxxx")

To delete a calendar item:

result = PatchRetention::CalendarItems.delete(calendar_item_id: "cal_xxxxxxxxxxxxxx")
# => { success: true }

To list calendar items:

# List all calendar items
calendar_items = PatchRetention::CalendarItems.all()

# List calendar items for a specific contact
calendar_items = PatchRetention::CalendarItems.all(contact_id: "ct_xxxxxxxxxxxxxx")

# List calendar items within ID range (MongoDB IDs)
calendar_items = PatchRetention::CalendarItems.all(
  min_id: "0123456789abcdefghijklmn", # Get all items on or after this ID
  max_id: "0123456789abcdefghijklmn"  # Get all items on or before this ID
)

# List specific calendar items by IDs (comma-separated)
calendar_items = PatchRetention::CalendarItems.all(
  id: "0123456789abcdefghijklmn,123456789abcdefghijklmn"
)

# Filter by date range (predefined options)
calendar_items = PatchRetention::CalendarItems.all(
  date_range: "Today"  # Options: "Today", "Yesterday", "Last 7 Days", "This Week", "Last Week", "Last 30 Days"
)

# With pagination
calendar_items = PatchRetention::CalendarItems.all(
  limit: 50,  # Default is 50, max is 100
  offset: 25  # Skip first 25 results
)

Development

After checking out the repo, run bin/setup to install dependencies.

Setting up Test Environment

To run tests, you'll need to set up test credentials. The test credentials should NOT be committed to the repository.

  1. Using environment variables (recommended):

    export PATCH_RETENTION_TEST_CLIENT_ID=your_test_client_id
    export PATCH_RETENTION_TEST_CLIENT_SECRET=your_test_client_secret
    
  2. Using the setup script:

    # This will create .env.test from your environment variables
    ./script/setup_test_env
    
  3. Manual setup: Create a .env.test file with:

    PATCH_RETENTION_CLIENT_ID=your_test_client_id
    PATCH_RETENTION_CLIENT_SECRET=your_test_client_secret
    PATCH_RETENTION_API_URL=https://api.patchretention.com/v2
    

Note: The .env.test file is gitignored and should never be committed.

Running Tests

Once your test environment is set up:

bundle exec rake spec

VCR Recording Modes

By default, VCR runs in :once mode, which only records HTTP interactions once. To re-record cassettes:

# Re-record all cassettes
VCR_RECORD_MODE=new_episodes bundle exec rake spec

# Never make real HTTP calls (useful for CI)
VCR_RECORD_MODE=none bundle exec rake spec

You can also run bin/console for an interactive prompt that will allow you to experiment.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/patch_retention. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the PatchRetention project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.