Patch Retention API Wrapper
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
Using Environment Files (Recommended for Development)
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.
Using environment variables (recommended):
export PATCH_RETENTION_TEST_CLIENT_ID=your_test_client_id export PATCH_RETENTION_TEST_CLIENT_SECRET=your_test_client_secretUsing the setup script:
# This will create .env.test from your environment variables ./script/setup_test_envManual setup: Create a
.env.testfile 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.