Ruby on Rails app that ingests listings from the Empire Flippers Marketplace API, stores them in PostgreSQL, and synchronizes currently For Sale listings into HubSpot deals on a daily schedule.
- Pulls listing data from the public Empire Flippers Listings API.
- Persists listings locally with idempotent upserts (no duplicate
listing_numberrows). - Syncs only
For Salelistings to HubSpot deals. - Prevents duplicate HubSpot deals by reusing stored HubSpot deal IDs and searching by deterministic deal name.
- Runs the full workflow daily using
sidekiq+sidekiq-scheduler.
- Daily orchestration job
DailyMarketplaceSyncJobruns:EmpireFlippers::ListingsImporterHubspotSync::DealsSyncService
- Idempotent listing storage
- Local uniqueness key:
listings.listing_number - Uses bulk upsert for safe reruns.
- Local uniqueness key:
- HubSpot sync with duplicate prevention
- Sync scope:
Listing.for_sale - Required mapping:
- Deal Name ->
Listing #{listing_number} - Amount ->
listing_price - Close Date -> now + 30 days
- Deal Description ->
summary
- Deal Name ->
- Duplicate controls:
- Reuse existing
hubspot_deal_idif still valid - Else search by
dealname - Else create
- Reuse existing
- Sync scope:
app/services/empire_flippers/client.rb- API client for Empire Flippers listings endpoint.
app/services/empire_flippers/listings_importer.rb- Paginates and upserts listing records.
app/services/hubspot_sync/client.rb- HubSpot request wrapper.
app/services/hubspot_sync/deals_sync_service.rb- Sync logic for For Sale listings and dedupe behavior.
app/jobs/daily_marketplace_sync_job.rb- Daily import + sync orchestration.
config/sidekiq.yml- Sidekiq queues and scheduler cron config.
lib/tasks/empire_flippers.rake- Manual tasks for import, HubSpot sync, and full daily run.
- Ruby 3.3+
- PostgreSQL
- Redis
- Bundler
bundle installCopy .env.example to .env and set values:
cp .env.example .envRequired:
HUBSPOT_ACCESS_TOKEN(PAT or OAuth access token with deals scopes)EMPIRE_FLIPPERS_BASE_URLEMPIRE_FLIPPERS_LISTINGS_PATHHUBSPOT_DEAL_CLOSE_DAYSREDIS_URLDAILY_MARKETPLACE_SYNC_CRON
bin/rails db:create db:migratebrew services start redisbundle exec rspecbin/rails empire_flippers:import_listingsbin/rails empire_flippers:sync_hubspot_dealsbin/rails empire_flippers:run_daily_syncbundle exec sidekiq -C config/sidekiq.yml- Fetch paginated listing payloads from Empire Flippers.
- Upsert into
listingstable keyed bylisting_number. - Select only
listing_status = "For Sale". - Build HubSpot deal properties from listing fields.
- Update existing HubSpot deal or create if not found.
- Persist
hubspot_deal_idlocally for future idempotent syncs.
bin/rails runner "puts({total: Listing.count, for_sale: Listing.for_sale.count, linked_to_hubspot: Listing.for_sale.where.not(hubspot_deal_id: nil).count}.to_json)"bin/rails runner "puts({duplicate_listing_numbers: Listing.group(:listing_number).having('COUNT(*) > 1').count.size, duplicate_hubspot_ids: Listing.where.not(hubspot_deal_id: nil).group(:hubspot_deal_id).having('COUNT(*) > 1').count.size}.to_json)"bin/rails runner "cfg = YAML.load(ERB.new(File.read(Rails.root.join('config/sidekiq.yml'))).result); puts cfg[:scheduler][:schedule].keys"RSpec includes:
Listingmodel validations and scopes- Empire Flippers importer:
- idempotent reruns
- updates on changed payload
- pagination handling
- invalid payload skip behavior
- HubSpot sync:
- create vs update behavior
- duplicate prevention strategy
- For Sale filtering
- Daily orchestration job call order
- This project is designed for local/demo execution and challenge validation.
- Daily execution requires Sidekiq + Redis running continuously.
- Keep secrets only in
.env(never commit tokens).