Document Version: 1.2 Date: October 3, 2025 Status: Phase 1 & 2 Complete ✅ - Phase 3 Next Last Updated: October 4, 2025
Before implementing Ramp virtual card integration for automated vendor payments, we must build several foundational workflows:
This document outlines what needs to be built, in what order, and why each piece is critical for Ramp integration.
Payment Infrastructure (Authorize.Net):
PaymentTransaction model with auth_only, capture, refund supportAuthorizeNetPaymentService with authorize/capture methodsCaptureAuthorizedPaymentsJob exists (captures authorized payments)Order.needing_capture scope (finds orders ready for capture)Vendor Portal:
Order Management:
Payment Flow:
CaptureAuthorizedPaymentsJob scheduled in recurring.yml (every 4 hours)Vendor Availability System:
vendor_blocked_dates, vendor_working_overridesminimum_advance_hours, default_working_daysImmediate Order Notifications:
Delivery Confirmation System:
delivery_confirmations table with fraud scoring fieldsVendor Workflows:
Portal Features:
Current Broken Flow:
Order Placed → [No authorization happening] → Delivery → [Manual capture?]
Required Flow:
1. Order Placed (CC payment)
→ Authorize payment (auth_only - hold funds)
→ Order status: "confirmed", payment_status: "authorized"
2. Delivery Date Arrives
→ CaptureAuthorizedPaymentsJob runs (cron: every 4 hours)
→ Finds orders: delivery_date <= today + payment_status = "authorized"
→ Captures held funds
→ Updates payment_status: "paid"
→ Sends delivery notification email to vendor
3. Next Day (Day After Delivery)
→ Send "Confirm Delivery" email to vendor
→ List all deliveries from previous day
→ Vendor confirms (triggers Ramp card creation)
Key Rules:
Trigger: Day AFTER delivery date
Email Content:
Subject: Confirm Yesterday's Deliveries - [Vendor Name]
Hi [Vendor Name],
Please confirm the following deliveries from [Yesterday's Date]:
┌─────────────────────────────────────────────────────────┐
│ Order #O20251003001 │
│ Product: 20-Yard Dumpster │
│ Address: 123 Main St, San Diego, CA 92101 │
│ Customer: ABC Construction │
│ │
│ [✓ Confirm Delivery] [✗ Report Issue] │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ Order #O20251003002 │
│ Product: Storage Container │
│ Address: 456 Oak Ave, San Diego, CA 92102 │
│ Customer: XYZ Company │
│ │
│ [✓ Confirm Delivery] [✗ Report Issue] │
└─────────────────────────────────────────────────────────┘
[Confirm All Deliveries]
Or view and confirm in your portal: https://vendors.quarryrents.com/confirmations
Two Confirmation Methods:
Method 1: Email Link (No Login - Token-Based)
Method 2: Vendor Portal (Login Required)
Fraud Detection:
After Confirmation:
Trigger: Vendor initiates pickup OR admin schedules pickup
Vendor-Initiated Flow:
1. Vendor logs into portal
→ Goes to "Active Orders"
→ Clicks "Request Pickup" on order
→ Fills out pickup request form:
- Preferred pickup date
- Reason (contract ended, customer requested, swap)
- Notes/special instructions
2. Admin Reviews Pickup Request
→ Receives notification
→ Checks customer account status
→ Approves or denies with reason
3. Pickup Day Arrives
→ Vendor completes pickup
→ Next day: Send "Confirm Pickup" email (same as delivery)
→ Vendor confirms pickup
4. Pickup Confirmed
→ Order status: "completed"
→ rental_end_date: set to pickup date
→ Cancel Ramp virtual card (if exists)
→ Stop recurring billing
→ Generate final invoice with overages
Admin-Initiated Flow:
1. Admin schedules pickup (existing functionality)
→ Sets pickup_date on order
→ Sends notification to vendor
2. Pickup Day + 1
→ Send confirmation email to vendor
→ Vendor confirms
3. After confirmation
→ Same as above (complete order, cancel card, etc.)
Key Rules:
Different from CC Customers:
1. PO Order Placed
→ No payment authorization (they'll pay later)
→ Order status: "confirmed", payment_method: "purchase_order"
2. Delivery Date Arrives
→ No capture needed (nothing to capture)
→ Generate invoice for customer
→ Send delivery notification to vendor
→ payment_status: "invoiced"
3. Next Day (Day After Delivery)
→ Send "Confirm Delivery" email to vendor
→ Vendor confirms
4. Vendor Confirms
→ Trigger Ramp card creation
→ Amount = vendor cost from invoice
→ Send card to vendor
Ramp Card Creation Logic:
delivery_confirmationscreate_table :delivery_confirmations do |t|
t.references :order, null: false, foreign_key: true
t.references :vendor, null: false, foreign_key: true
t.string :confirmation_type, null: false # delivery, pickup
t.string :confirmation_method, null: false # email_token, portal_login
t.string :confirmation_token # For email-based confirmation
t.datetime :token_expires_at
t.datetime :confirmed_at
t.string :confirmed_ip
t.text :confirmed_user_agent
t.string :confirmed_location # GPS coords if available
t.boolean :is_fraudulent, default: false
t.decimal :confidence_score, precision: 5, scale: 2 # 0-100
t.text :fraud_notes
t.text :vendor_notes
t.string :status, default: 'pending' # pending, confirmed, disputed, expired
t.timestamps
end
add_index :delivery_confirmations, :confirmation_token, unique: true
add_index :delivery_confirmations, :status
add_index :delivery_confirmations, :confirmed_at
pickup_requestscreate_table :pickup_requests do |t|
t.references :order, null: false, foreign_key: true
t.references :vendor, null: false, foreign_key: true
t.references :requested_by_user, foreign_key: { to_table: :users }, null: true
t.date :requested_pickup_date, null: false
t.string :reason, null: false # contract_ended, customer_requested, swap
t.text :vendor_notes
t.text :admin_notes
t.string :status, default: 'pending' # pending, approved, denied, completed
t.references :reviewed_by_user, foreign_key: { to_table: :users }
t.datetime :reviewed_at
t.text :review_notes
t.timestamps
end
add_index :pickup_requests, :status
add_index :pickup_requests, :requested_pickup_date
orders table# Already has: delivery_date, pickup_date, rental_start_date, rental_end_date
add_column :orders, :delivery_confirmed_at, :datetime
add_column :orders, :delivery_confirmed_by_vendor, :boolean, default: false
add_column :orders, :pickup_confirmed_at, :datetime
add_column :orders, :pickup_confirmed_by_vendor, :boolean, default: false
add_column :orders, :capture_attempted_at, :datetime
add_column :orders, :capture_failed_reason, :text
Priority: CRITICAL - Blocking everything else
Files to modify:
app/controllers/orders_controller.rb (or wherever checkout happens)authorize_payment instead of charge_payment_profileCurrent (broken):
# Likely doing this (immediate charge):
service.charge_payment_profile(order: order, payment_profile: profile)
Required (hold funds):
# Do this instead (authorize only):
auth_transaction = service.authorize_payment(order: order, payment_profile: profile)
if auth_transaction&.successful?
order.update(payment_status: 'authorized')
else
# Handle failure
end
File: config/recurring.yml
Add:
capture_authorized_payments:
class: CaptureAuthorizedPaymentsJob
queue: default
schedule: every 4 hours # Runs 6 times per day
Why every 4 hours?
File: app/jobs/capture_authorized_payments_job.rb
Add after successful capture:
if capture_transaction
order.update(
payment_status: "paid",
capture_attempted_at: Time.current
)
# Send delivery notification to vendor
VendorMailer.delivery_notification(order).deliver_later
Rails.logger.info "Successfully captured..."
else
# Add failure tracking
order.update(
capture_attempted_at: Time.current,
capture_failed_reason: service.errors.join(', ')
)
# Alert admin
AdminMailer.capture_failed(order, service.errors).deliver_later
end
CaptureAuthorizedPaymentsJob.perform_nowEstimated Time: 2-3 days
What Was Built:
Payment Authorization on Checkout (app/services/authorize_net_payment_service.rb:257)
AuthCaptureTransaction to AuthOnlyTransactionController Updated (app/controllers/app/quotes_controller.rb:288)
Automated Capture Scheduled (config/recurring.yml:32-35)
CaptureAuthorizedPaymentsJob added to recurring jobsEnhanced Capture Job (app/jobs/capture_authorized_payments_job.rb:33-56)
capture_attempted_at timestampcapture_failed_reason textVendor Delivery Notification (app/mailers/vendor_mailer.rb)
delivery_notification(order) methodDatabase Migration (db/migrate/20251004150929_add_capture_tracking_to_orders.rb)
capture_attempted_at column (datetime)capture_failed_reason column (text)Current Flow:
1. Customer places order with CC → Auth-only transaction
2. Order.payment_status = "authorized" (funds held)
3. Delivery date arrives
4. CaptureAuthorizedPaymentsJob runs (every 4 hours)
5. Finds orders with delivery_date <= today + status "authorized"
6. Captures payment → Order.payment_status = "paid"
7. Sends email to vendor: "Delivery Today - [Product] to [Customer]"
Testing Status: Ready for production deployment
Next: ✅ COMPLETE - Phase 2 implemented (see below)
Priority: HIGH - Required for Ramp trigger Status: ✅ Implementation Complete (October 4, 2025)
What Was Built:
Vendor Availability System (NEW - Added per business request)
db/migrate/20251004181413_add_availability_rules_to_vendors.rbdb/migrate/20251004181418_create_vendor_blocked_dates.rbdb/migrate/20251004181423_create_vendor_working_overrides.rbVendorBlockedDate, VendorWorkingOverrideconfig/initializers/holidays.rbapp/controllers/vendors/availability_controller.rbapp/views/vendors/availability/index.html.erbhttps://vendors.quarryrents.com/availabilityImmediate Order Notifications (NEW - Added per business request)
VendorMailer.order_created - Instant notification when order placedVendorMailer.order_cancelled - Instant notification when order cancelledapp/views/vendor_mailer/order_created.{html,text}.erbapp/views/vendor_mailer/order_cancelled.{html,text}.erbDelivery Confirmation Database
db/migrate/20251004182139_create_delivery_confirmations.rbdb/migrate/20251004182201_add_confirmation_fields_to_orders.rbapp/models/delivery_confirmation.rb (with fraud detection)Delivery Confirmation Email System
app/jobs/send_delivery_confirmation_emails_job.rbconfig/recurring.yml (daily at 8am)VendorMailer.delivery_confirmationsapp/views/vendor_mailer/delivery_confirmations.{html,text}.erbDelivery Confirmation Portal & Public Endpoints
app/controllers/vendors/confirmations_controller.rbapp/views/vendors/confirmations/index.html.erbconfirmed.html.erb - Success pagealready_confirmed.html.erb - Already processedexpired.html.erb - Token expiredinvalid_token.html.erb - Invalid tokenerror.html.erb - Error handlingbulk_confirmed.html.erb - Bulk successreport_issue.html.erb - Issue reportingconfig/routes.rbPortal URLs:
https://vendors.quarryrents.com/confirmations - Dashboard (requires login)https://vendors.quarryrents.com/confirm/:token - Email confirmation (no login)https://vendors.quarryrents.com/confirm-all - Bulk confirmation (no login)https://vendors.quarryrents.com/report-issue/:token - Report issue (no login)Fraud Detection Features:
Ready for Phase 4:
Priority: HIGH - Required for Ramp trigger
bin/rails generate migration CreateDeliveryConfirmations
bin/rails generate migration AddConfirmationFieldsToOrders
File: app/models/delivery_confirmation.rb
class DeliveryConfirmation < ApplicationRecord
belongs_to :order
belongs_to :vendor
CONFIRMATION_TYPES = %w[delivery pickup].freeze
CONFIRMATION_METHODS = %w[email_token portal_login].freeze
STATUSES = %w[pending confirmed disputed expired].freeze
validates :confirmation_type, inclusion: { in: CONFIRMATION_TYPES }
validates :confirmation_method, inclusion: { in: CONFIRMATION_METHODS }
validates :status, inclusion: { in: STATUSES }
scope :pending, -> { where(status: 'pending') }
scope :confirmed, -> { where(status: 'confirmed') }
scope :deliveries, -> { where(confirmation_type: 'delivery') }
scope :pickups, -> { where(confirmation_type: 'pickup') }
scope :fraudulent, -> { where(is_fraudulent: true) }
before_create :generate_confirmation_token
before_create :set_token_expiration
def confirm!(ip_address:, user_agent:, location: nil, method:)
update!(
status: 'confirmed',
confirmed_at: Time.current,
confirmed_ip: ip_address,
confirmed_user_agent: user_agent,
confirmed_location: location,
confirmation_method: method
)
# Calculate fraud score
calculate_fraud_score!
# Trigger downstream actions
trigger_post_confirmation_actions
end
def expired?
token_expires_at.present? && token_expires_at < Time.current
end
private
def generate_confirmation_token
self.confirmation_token = SecureRandom.urlsafe_base64(32)
end
def set_token_expiration
self.token_expires_at = 7.days.from_now
end
def calculate_fraud_score!
score = 100.0
# Reduce score for suspicious patterns
if confirmed_at && order.delivery_date
hours_since_delivery = (confirmed_at - order.delivery_date.to_time) / 1.hour
# Too fast (confirmed same day as delivery)
score -= 30 if hours_since_delivery < 12
# Too late (confirmed more than 3 days later)
score -= 20 if hours_since_delivery > 72
end
# Check for bulk confirmations (same IP, same minute)
same_minute_confirmations = DeliveryConfirmation
.where(vendor: vendor)
.where(confirmed_ip: confirmed_ip)
.where('confirmed_at BETWEEN ? AND ?', confirmed_at - 1.minute, confirmed_at + 1.minute)
.count
score -= 25 if same_minute_confirmations > 5 # Bulk confirming
update!(
confidence_score: [score, 0].max,
is_fraudulent: score < 50
)
end
def trigger_post_confirmation_actions
case confirmation_type
when 'delivery'
handle_delivery_confirmed
when 'pickup'
handle_pickup_confirmed
end
end
def handle_delivery_confirmed
order.update!(
delivery_confirmed_at: confirmed_at,
delivery_confirmed_by_vendor: true
)
# Trigger Ramp card creation
CreateVendorPaymentCardJob.perform_later(order.id)
end
def handle_pickup_confirmed
order.update!(
pickup_confirmed_at: confirmed_at,
pickup_confirmed_by_vendor: true,
status: 'completed',
rental_end_date: confirmed_at.to_date
)
# Cancel Ramp card if exists
CancelVendorPaymentCardJob.perform_later(order.id)
# Stop recurring billing
order.stop_recurring_billing! if order.recurring?
end
end
File: app/jobs/send_delivery_confirmation_emails_job.rb
class SendDeliveryConfirmationEmailsJob < ApplicationJob
queue_as :default
def perform
# Find all deliveries from yesterday that haven't been confirmed
yesterday = Date.yesterday
delivered_orders = Order
.where(delivery_date: yesterday)
.where(delivery_confirmed_by_vendor: false)
.where.not(status: ['cancelled', 'quote'])
.includes(:vendor, :product, :contact, :company)
# Group by vendor
orders_by_vendor = delivered_orders.group_by(&:vendor)
orders_by_vendor.each do |vendor, orders|
# Create confirmation records
confirmations = orders.map do |order|
DeliveryConfirmation.create!(
order: order,
vendor: vendor,
confirmation_type: 'delivery',
confirmation_method: 'email_token', # Will be updated if confirmed via portal
status: 'pending'
)
end
# Send batch email to vendor
VendorMailer.delivery_confirmations(vendor, confirmations).deliver_later
end
Rails.logger.info "Sent delivery confirmation emails to #{orders_by_vendor.count} vendors for #{delivered_orders.count} orders"
end
end
File: config/recurring.yml
send_delivery_confirmations:
class: SendDeliveryConfirmationEmailsJob
queue: default
schedule: at 8am every day # Morning after delivery
File: app/mailers/vendor_mailer.rb
class VendorMailer < ApplicationMailer
def delivery_notification(order)
@order = order
@vendor = order.vendor
@product = order.product
@customer = order.company || order.contact
mail(
to: @vendor.email,
subject: "Delivery Today - #{@product.name} to #{@customer.name}"
)
end
def delivery_confirmations(vendor, confirmations)
@vendor = vendor
@confirmations = confirmations
@orders = confirmations.map(&:order)
mail(
to: vendor.email,
subject: "Confirm Yesterday's Deliveries (#{confirmations.count})"
)
end
def pickup_confirmations(vendor, confirmations)
@vendor = vendor
@confirmations = confirmations
@orders = confirmations.map(&:order)
mail(
to: vendor.email,
subject: "Confirm Yesterday's Pickups (#{confirmations.count})"
)
end
end
File: app/views/vendor_mailer/delivery_confirmations.html.erb
<h2>Please Confirm Yesterday's Deliveries</h2>
<p>Hi <%= @vendor.name %>,</p>
<p>Please confirm the following deliveries from <%= Date.yesterday.strftime('%B %d, %Y') %>:</p>
<% @confirmations.each do |confirmation| %>
<% order = confirmation.order %>
<div style="border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 5px;">
<h3>Order #<%= order.order_number %></h3>
<p><strong>Product:</strong> <%= order.product.name %></p>
<p><strong>Delivered To:</strong><br>
<%= order.delivery_address %><br>
<%= order.delivery_city %>, <%= order.delivery_state %> <%= order.delivery_zipcode %>
</p>
<p><strong>Customer:</strong> <%= order.customer_name %></p>
<p>
<a href="<%= confirm_delivery_vendor_url(token: confirmation.confirmation_token) %>"
style="background: #10B981; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block;">
✓ Confirm Delivery
</a>
<a href="<%= report_issue_vendor_url(token: confirmation.confirmation_token) %>"
style="background: #EF4444; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px; display: inline-block; margin-left: 10px;">
✗ Report Issue
</a>
</p>
</div>
<% end %>
<p style="margin-top: 30px;">
<a href="<%= confirm_all_deliveries_vendor_url(vendor_id: @vendor.id, date: Date.yesterday) %>"
style="background: #3B82F6; color: white; padding: 12px 30px; text-decoration: none; border-radius: 5px; display: inline-block; font-size: 16px;">
Confirm All Deliveries
</a>
</p>
<p style="margin-top: 20px;">
Or view and confirm in your portal:
<a href="<%= vendors_confirmations_url %>">
https://vendors.quarryrents.com/confirmations
</a>
</p>
<p style="color: #6B7280; font-size: 12px; margin-top: 30px;">
This confirmation link expires in 7 days.
</p>
File: app/controllers/vendors/confirmations_controller.rb
class Vendors::ConfirmationsController < Vendors::BaseController
skip_before_action :require_vendor_authentication, only: [:confirm_delivery, :report_issue]
# Portal view (requires login)
def index
@pending_confirmations = current_vendor.delivery_confirmations
.pending
.includes(order: [:product, :contact, :company])
.order(created_at: :desc)
@confirmed_today = current_vendor.delivery_confirmations
.confirmed
.where('confirmed_at >= ?', Date.current.beginning_of_day)
.count
end
# Portal confirm (requires login)
def confirm
confirmation = current_vendor.delivery_confirmations.find(params[:id])
if confirmation.expired?
redirect_to vendors_confirmations_path, alert: "This confirmation has expired."
return
end
confirmation.confirm!(
ip_address: request.remote_ip,
user_agent: request.user_agent,
method: 'portal_login'
)
redirect_to vendors_confirmations_path, notice: "Delivery confirmed successfully."
end
# Email link confirm (no login required - uses token)
def confirm_delivery
confirmation = DeliveryConfirmation.find_by(confirmation_token: params[:token])
unless confirmation
render plain: "Invalid confirmation link.", status: :not_found
return
end
if confirmation.expired?
render plain: "This confirmation link has expired.", status: :gone
return
end
if confirmation.confirmed?
render plain: "This delivery has already been confirmed.", status: :ok
return
end
confirmation.confirm!(
ip_address: request.remote_ip,
user_agent: request.user_agent,
method: 'email_token'
)
render :confirmed, locals: { confirmation: confirmation }
end
# Bulk confirm via email
def confirm_all_deliveries
vendor = Vendor.find(params[:vendor_id])
date = Date.parse(params[:date])
confirmations = vendor.delivery_confirmations
.pending
.joins(:order)
.where(orders: { delivery_date: date })
confirmations.each do |confirmation|
confirmation.confirm!(
ip_address: request.remote_ip,
user_agent: request.user_agent,
method: 'email_token'
)
end
render plain: "Successfully confirmed #{confirmations.count} deliveries."
end
def report_issue
confirmation = DeliveryConfirmation.find_by(confirmation_token: params[:token])
unless confirmation
render plain: "Invalid link.", status: :not_found
return
end
# Redirect to issue reporting form
redirect_to new_vendors_delivery_issue_path(confirmation_id: confirmation.id)
end
end
File: config/routes.rb
# Vendor Portal
constraints subdomain: "vendors" do
namespace :vendors, path: "/" do
# ... existing routes ...
resources :confirmations, only: [:index] do
member do
post :confirm
end
end
# Public confirmation endpoints (no auth required)
get 'confirm/:token', to: 'confirmations#confirm_delivery', as: :confirm_delivery_vendor
get 'confirm-all', to: 'confirmations#confirm_all_deliveries', as: :confirm_all_deliveries_vendor
get 'report-issue/:token', to: 'confirmations#report_issue', as: :report_issue_vendor
end
end
Estimated Time: 5-7 days
Priority: HIGH - Required for Ramp card cancellation
File: app/models/pickup_request.rb
class PickupRequest < ApplicationRecord
belongs_to :order
belongs_to :vendor
belongs_to :requested_by_user, class_name: 'User', optional: true
belongs_to :reviewed_by_user, class_name: 'User', optional: true
STATUSES = %w[pending approved denied completed].freeze
REASONS = %w[contract_ended customer_requested swap equipment_issue].freeze
validates :status, inclusion: { in: STATUSES }
validates :reason, inclusion: { in: REASONS }
validates :requested_pickup_date, presence: true
scope :pending, -> { where(status: 'pending') }
scope :approved, -> { where(status: 'approved') }
scope :denied, -> { where(status: 'denied') }
scope :completed, -> { where(status: 'completed') }
def approve!(user:, notes: nil)
transaction do
update!(
status: 'approved',
reviewed_by_user: user,
reviewed_at: Time.current,
review_notes: notes
)
order.update!(pickup_date: requested_pickup_date)
# Send notification to vendor
VendorMailer.pickup_approved(self).deliver_later
end
end
def deny!(user:, notes:)
update!(
status: 'denied',
reviewed_by_user: user,
reviewed_at: Time.current,
review_notes: notes
)
# Send notification to vendor
VendorMailer.pickup_denied(self).deliver_later
end
end
File: app/controllers/vendors/pickup_requests_controller.rb
class Vendors::PickupRequestsController < Vendors::BaseController
def index
@pickup_requests = current_vendor.pickup_requests
.includes(order: [:product, :contact, :company])
.order(created_at: :desc)
end
def new
@order = current_vendor.orders.find(params[:order_id])
@pickup_request = PickupRequest.new(order: @order, vendor: current_vendor)
end
def create
@order = current_vendor.orders.find(params[:order_id])
@pickup_request = PickupRequest.new(pickup_request_params)
@pickup_request.order = @order
@pickup_request.vendor = current_vendor
if @pickup_request.save
# Notify admin
AdminMailer.pickup_request_submitted(@pickup_request).deliver_later
redirect_to vendors_pickup_requests_path,
notice: "Pickup request submitted. Admin will review shortly."
else
render :new, status: :unprocessable_entity
end
end
private
def pickup_request_params
params.require(:pickup_request).permit(
:requested_pickup_date,
:reason,
:vendor_notes
)
end
end
File: app/controllers/admin/pickup_requests_controller.rb
class Admin::PickupRequestsController < ApplicationController
before_action :set_pickup_request, only: [:show, :approve, :deny]
def index
@pickup_requests = PickupRequest.includes(:order, :vendor)
.order(created_at: :desc)
@pickup_requests = @pickup_requests.pending if params[:status] == 'pending'
end
def show
end
def approve
if @pickup_request.approve!(user: current_user, notes: params[:notes])
redirect_to admin_pickup_requests_path, notice: "Pickup request approved."
else
redirect_to admin_pickup_request_path(@pickup_request),
alert: "Failed to approve pickup request."
end
end
def deny
if @pickup_request.deny!(user: current_user, notes: params[:notes])
redirect_to admin_pickup_requests_path, notice: "Pickup request denied."
else
redirect_to admin_pickup_request_path(@pickup_request),
alert: "Failed to deny pickup request."
end
end
private
def set_pickup_request
@pickup_request = PickupRequest.find(params[:id])
end
end
File: app/jobs/send_pickup_confirmation_emails_job.rb
class SendPickupConfirmationEmailsJob < ApplicationJob
queue_as :default
def perform
# Find all pickups from yesterday that haven't been confirmed
yesterday = Date.yesterday
picked_up_orders = Order
.where(pickup_date: yesterday)
.where(pickup_confirmed_by_vendor: false)
.where.not(status: ['cancelled', 'quote'])
.includes(:vendor, :product, :contact, :company)
# Group by vendor
orders_by_vendor = picked_up_orders.group_by(&:vendor)
orders_by_vendor.each do |vendor, orders|
# Create confirmation records
confirmations = orders.map do |order|
DeliveryConfirmation.create!(
order: order,
vendor: vendor,
confirmation_type: 'pickup',
confirmation_method: 'email_token',
status: 'pending'
)
end
# Send batch email to vendor
VendorMailer.pickup_confirmations(vendor, confirmations).deliver_later
end
end
end
File: config/recurring.yml
send_pickup_confirmations:
class: SendPickupConfirmationEmailsJob
queue: default
schedule: at 8am every day
Estimated Time: 4-5 days
Priority: MEDIUM - After phases 1-3 complete
This is where we finally integrate Ramp, but ONLY after all the prerequisites are done.
File: app/jobs/create_vendor_payment_card_job.rb
class CreateVendorPaymentCardJob < ApplicationJob
queue_as :default
def perform(order_id)
order = Order.find(order_id)
# Verify prerequisites
unless can_create_card?(order)
Rails.logger.warn "Cannot create card for order #{order.order_number}: prerequisites not met"
return
end
# Determine card type and amount
card_type = determine_card_type(order)
card_amount = calculate_card_amount(order)
# Create card via Ramp
service = RampCardService.new
if card_type == :one_time
card = service.create_one_time_card(
order: order,
amount: card_amount,
vendor: order.vendor
)
else
card = service.create_recurring_card(
order: order,
monthly_amount: card_amount,
vendor: order.vendor
)
end
if card
# Send card details to vendor (one-time view)
VendorPaymentCardMailer.card_ready(order, card).deliver_later
Rails.logger.info "Created #{card_type} Ramp card for order #{order.order_number}"
else
Rails.logger.error "Failed to create Ramp card for order #{order.order_number}"
AdminMailer.card_creation_failed(order).deliver_later
end
end
private
def can_create_card?(order)
# Must be confirmed delivery
return false unless order.delivery_confirmed_by_vendor?
# Payment must be verified
case order.payment_method
when 'credit_card'
return false unless order.payment_status == 'paid'
when 'purchase_order'
return false unless order.payment_status == 'invoiced'
end
# Must have vendor cost info
return false unless order.vendor_base_cost.present?
true
end
def determine_card_type(order)
order.recurring? || order.vendor_recurring? ? :recurring : :one_time
end
def calculate_card_amount(order)
# First month includes delivery fees
if first_month_for_order?(order)
base = order.vendor_base_cost || 0
delivery = order.vendor.delivery_fee_for_zipcode(order.delivery_zipcode) || 0
setup = order.setup_fee || 0 # If vendor charges setup
total = base + delivery + setup
total * 1.10 # 10% buffer for first month
else
# Recurring months: just base cost
(order.vendor_base_cost || 0) * 1.05 # 5% buffer
end
end
def first_month_for_order?(order)
return true unless order.recurring?
# Check if this is first billing period
order.vendor_billing_periods.count.zero?
end
end
File: app/services/ramp_card_service.rb
class RampCardService
# ... existing code ...
def create_one_time_card(order:, amount:, vendor:)
# Card expires 30 days after pickup
expires_at = (order.pickup_date || order.delivery_date + 14.days) + 30.days
ramp_response = @ramp_client.create_virtual_card(
cardholder_id: vendor.ramp_cardholder_id,
display_name: "#{vendor.name} - Order ##{order.order_number}",
spending_limit: {
amount: (amount * 100).to_i, # Convert to cents
interval: 'TOTAL'
}
)
card = RampVirtualCard.create!(
vendor: vendor,
order: order,
ramp_card_id: ramp_response['id'],
card_last_four: ramp_response['last_four'],
card_name: "Order ##{order.order_number}",
spending_limit: amount,
status: 'active',
strategy: 'per_order',
ramp_card_type: 'one_time',
expires_at: expires_at
)
card
end
def create_recurring_card(order:, monthly_amount:, vendor:)
ramp_response = @ramp_client.create_virtual_card(
cardholder_id: vendor.ramp_cardholder_id,
display_name: "#{vendor.name} - #{order.delivery_zipcode} (Recurring)",
spending_limit: {
amount: (monthly_amount * 100).to_i,
interval: 'MONTHLY' # Limit resets monthly
}
)
card = RampVirtualCard.create!(
vendor: vendor,
order: order,
ramp_card_id: ramp_response['id'],
card_last_four: ramp_response['last_four'],
card_name: "#{order.delivery_zipcode} Recurring",
spending_limit: monthly_amount,
status: 'active',
strategy: 'per_location',
ramp_card_type: 'recurring',
location_identifier: order.delivery_zipcode,
expires_at: nil # No expiration for recurring
)
card
end
end
Uses Solid Cache instead of Redis
File: app/services/card_view_token_service.rb
class CardViewTokenService
CACHE_PREFIX = "card_view_token"
TOKEN_TTL = 1.hour
def self.generate_token_for_card(card)
token = SecureRandom.urlsafe_base64(32)
# Store in Solid Cache with TTL
Rails.cache.write(
"#{CACHE_PREFIX}:#{token}",
{ card_id: card.id, viewed: false },
expires_in: TOKEN_TTL
)
token
end
def self.validate_and_consume_token(token)
cache_key = "#{CACHE_PREFIX}:#{token}"
data = Rails.cache.read(cache_key)
return nil unless data
return nil if data[:viewed] # Already viewed
# Mark as viewed (atomic operation)
Rails.cache.write(
cache_key,
data.merge(viewed: true),
expires_in: TOKEN_TTL
)
RampVirtualCard.find(data[:card_id])
end
def self.token_status(token)
cache_key = "#{CACHE_PREFIX}:#{token}"
data = Rails.cache.read(cache_key)
return :invalid unless data
return :viewed if data[:viewed]
:valid
end
end
File: app/controllers/vendors/card_view_controller.rb
class Vendors::CardViewController < ApplicationController
skip_before_action :require_authentication
layout 'vendor_public'
def show
token = params[:token]
@token_status = CardViewTokenService.token_status(token)
case @token_status
when :invalid
render :expired
when :viewed
render :already_viewed
when :valid
@token = token
render :warning # Show warning before revealing card
end
end
def reveal
token = params[:token]
@card = CardViewTokenService.validate_and_consume_token(token)
unless @card
render plain: "Invalid or expired token", status: :forbidden
return
end
# Log the view
CardViewLog.create!(
ramp_virtual_card: @card,
viewed_at: Time.current,
ip_address: request.remote_ip,
user_agent: request.user_agent
)
# Fetch full card details from Ramp
ramp_client = RampApiClient.new
@card_details = ramp_client.get_card_details(@card.ramp_card_id)
# Card details will auto-clear from DOM after 2 minutes (via Stimulus)
render :reveal
end
end
Estimated Time: 5-7 days
┌─────────────────────────────────────────────────────────────┐
│ Order Placed (CC) │
│ ↓ │
│ AuthorizeNetPaymentService.authorize_payment │
│ ↓ │
│ PaymentTransaction: type = auth_only, status = approved │
│ ↓ │
│ Order: payment_status = "authorized" │
└─────────────────────────────────────────────────────────────┘
↓ (delivery_date arrives)
┌─────────────────────────────────────────────────────────────┐
│ CaptureAuthorizedPaymentsJob (every 4 hours) │
│ ↓ │
│ Find orders: delivery_date <= today + status = authorized │
│ ↓ │
│ AuthorizeNetPaymentService.capture_authorization │
│ ↓ │
│ PaymentTransaction: type = capture, status = approved │
│ ↓ │
│ Order: payment_status = "paid" │
│ ↓ │
│ VendorMailer.delivery_notification → Vendor │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Day After Delivery (8am) │
│ ↓ │
│ SendDeliveryConfirmationEmailsJob │
│ ↓ │
│ Find orders: delivery_date = yesterday │
│ + delivery_confirmed = false │
│ ↓ │
│ Create DeliveryConfirmation records │
│ ↓ │
│ VendorMailer.delivery_confirmations → Vendor │
│ - Email contains: │
│ • List of deliveries │
│ • "Confirm" button for each (token link) │
│ • "Confirm All" button │
│ • Link to portal │
└─────────────────────────────────────────────────────────────┘
↓ (vendor clicks link)
┌─────────────────────────────────────────────────────────────┐
│ Method 1: Email Token (No Login) │
│ ↓ │
│ GET /vendors/confirm/:token │
│ ↓ │
│ DeliveryConfirmation.confirm! │
│ - Records: IP, user agent, timestamp │
│ - Calculates fraud score │
│ - Updates order: delivery_confirmed = true │
│ ↓ │
│ Triggers: CreateVendorPaymentCardJob │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Method 2: Portal Login │
│ ↓ │
│ Vendor logs into vendors.quarryrents.com │
│ ↓ │
│ /vendors/confirmations - Shows pending confirmations │
│ ↓ │
│ Click "Confirm" → Same process as email │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ DeliveryConfirmation.confirm! (vendor confirms delivery) │
│ ↓ │
│ CreateVendorPaymentCardJob.perform_later(order.id) │
│ ↓ │
│ Verify prerequisites: │
│ ✓ Delivery confirmed by vendor │
│ ✓ Payment captured (CC) OR invoiced (PO) │
│ ✓ Vendor cost info present │
│ ↓ │
│ Calculate card amount: │
│ - First month: base + delivery + setup + 10% buffer │
│ - Recurring: base + 5% buffer │
│ ↓ │
│ RampCardService.create_card (one_time OR recurring) │
│ ↓ │
│ Create RampVirtualCard record │
│ ↓ │
│ Generate one-time view token (Solid Cache, 1 hour TTL) │
│ ↓ │
│ VendorPaymentCardMailer.card_ready → Vendor │
│ "Your payment card is ready - View once: [LINK]" │
└─────────────────────────────────────────────────────────────┘
↓ (vendor clicks link)
┌─────────────────────────────────────────────────────────────┐
│ GET /vendors/cards/view/:token │
│ ↓ │
│ Show WARNING page: │
│ "You can only view this card ONCE" │
│ [checkbox] I understand │
│ [Reveal Card Details] button │
│ ↓ │
│ POST /vendors/cards/reveal │
│ ↓ │
│ CardViewTokenService.validate_and_consume_token │
│ - Checks if already viewed (atomic check) │
│ - Marks token as consumed │
│ ↓ │
│ Fetch card details from Ramp API │
│ ↓ │
│ Display card: │
│ • Card number │
│ • CVV │
│ • Expiration │
│ • Spending limit │
│ • Copy-to-clipboard buttons │
│ ↓ │
│ Stimulus controller auto-clears after 2 minutes │
│ ↓ │
│ Log view: CardViewLog (IP, timestamp, user agent) │
└─────────────────────────────────────────────────────────────┘
# Test auth_only on order placement
1. Create test order with credit card
2. Verify PaymentTransaction.transaction_type = "auth_only"
3. Verify Order.payment_status = "authorized"
4. Check Authorize.Net dashboard - should show "Auth Only"
# Test capture on delivery date
1. Set order delivery_date to Date.current
2. Run: CaptureAuthorizedPaymentsJob.perform_now
3. Verify PaymentTransaction.transaction_type = "capture"
4. Verify Order.payment_status = "paid"
5. Check Authorize.Net dashboard - should show "Captured"
6. Verify VendorMailer.delivery_notification sent
# Test confirmation email generation
1. Create orders with delivery_date = Date.yesterday
2. Run: SendDeliveryConfirmationEmailsJob.perform_now
3. Verify DeliveryConfirmation records created
4. Check email sent to vendor with token links
# Test token-based confirmation
1. Extract token from email
2. Visit: /vendors/confirm/[TOKEN]
3. Verify confirmation recorded
4. Verify fraud score calculated
5. Verify CreateVendorPaymentCardJob triggered
# Test portal confirmation
1. Login as vendor
2. Visit: /vendors/confirmations
3. See pending confirmations
4. Click confirm
5. Verify same outcomes as token method
# Test fraud detection
1. Confirm delivery immediately (same day)
2. Verify low confidence_score
3. Bulk confirm 10 orders in same minute
4. Verify is_fraudulent = true
# Test pickup request
1. Login as vendor
2. Click "Request Pickup" on active order
3. Fill form, submit
4. Verify AdminMailer notification sent
5. Admin approves request
6. Verify order.pickup_date set
# Test pickup confirmation
1. Set order pickup_date = Date.yesterday
2. Run: SendPickupConfirmationEmailsJob.perform_now
3. Vendor confirms pickup
4. Verify order.status = "completed"
5. Verify CancelVendorPaymentCardJob triggered
6. Verify recurring billing stopped
# Test one-time card creation
1. Create order with delivery confirmation
2. Verify payment captured
3. Confirm delivery
4. Verify CreateVendorPaymentCardJob triggered
5. Check Ramp dashboard - card created
6. Verify email sent with view token
# Test card viewing
1. Extract token from email
2. Visit view URL
3. See warning page
4. Reveal card details
5. Verify card data shown
6. Refresh page - should say "already viewed"
7. Verify CardViewLog created
# Test recurring card
1. Create recurring order
2. Complete delivery confirmation
3. Verify recurring card created (no expiration)
4. Verify monthly limit with auto-reset
production:
# ... existing jobs ...
# Payment capture (every 4 hours)
capture_authorized_payments:
class: CaptureAuthorizedPaymentsJob
queue: default
schedule: every 4 hours
# Delivery confirmations (daily at 8am)
send_delivery_confirmations:
class: SendDeliveryConfirmationEmailsJob
queue: default
schedule: at 8am every day
# Pickup confirmations (daily at 8am)
send_pickup_confirmations:
class: SendPickupConfirmationEmailsJob
queue: default
schedule: at 8am every day
# Ensure HTTPS
config.force_ssl = true
# Parameter filtering (never log sensitive data)
config.filter_parameters += [
:card_number, :cvv, :card_code, :expiration_month, :expiration_year,
:payment_nonce, :cardholder_name, :ramp_card_number
]
# Authorize.Net
authorize_net:
api_login_id: <%= ENV['AUTHORIZE_NET_API_LOGIN_ID'] %>
transaction_key: <%= ENV['AUTHORIZE_NET_TRANSACTION_KEY'] %>
environment: production # or sandbox
# Ramp (for Phase 4)
ramp:
client_id: <%= ENV['RAMP_CLIENT_ID'] %>
client_secret: <%= ENV['RAMP_CLIENT_SECRET'] %>
api_url: https://api.ramp.com/v1
vault_api_url: https://vault.ramp.com/v1
webhook_secret: <%= ENV['RAMP_WEBHOOK_SECRET'] %>
authorize_payment (not charge)CaptureAuthorizedPaymentsJob to recurring.ymlRisk: Authorizations expire after 30 days Mitigation:
Risk: Vendor confirms deliveries they didn't make Mitigation:
Risk: Card creation fails, vendor can't get paid Mitigation:
Risk: Vendor shares token, multiple people view card Mitigation:
Authorization Hold Period:
PO Customer Invoicing:
Vendor Onboarding:
Recurring Card Termination:
Overage Handling:
Document Status: DRAFT - Awaiting Review Next Review Date: TBD Owner: Development Team