Ramp Virtual Card Integration - Prerequisites & Implementation Plan

Document Version: 1.2 Date: October 3, 2025 Status: Phase 1 & 2 Complete ✅ - Phase 3 Next Last Updated: October 4, 2025


Executive Summary

Before implementing Ramp virtual card integration for automated vendor payments, we must build several foundational workflows:

  1. Payment Authorization/Capture Flow - Auth on order placement, capture on delivery
  2. Vendor Delivery Confirmation - Vendor confirms deliveries to trigger payments
  3. Vendor Pickup/Removal Workflow - Vendor initiates pickup, triggers card cancellation
  4. Vendor Portal Enhancements - Delivery/pickup confirmation UI

This document outlines what needs to be built, in what order, and why each piece is critical for Ramp integration.


Current State Assessment

✅ What We Have

Payment Infrastructure (Authorize.Net):

Vendor Portal:

Order Management:

✅ Phase 1 Complete (October 4, 2025)

Payment Flow:

✅ Phase 2 Complete (October 4, 2025)

Vendor Availability System:

Immediate Order Notifications:

Delivery Confirmation System:

❌ What's Still Missing (Blockers for Ramp)

Vendor Workflows:

Portal Features:


Business Requirements

Payment Capture Flow (Credit Card Customers)

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:


Vendor Delivery Confirmation System

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:


Vendor Pickup/Removal Workflow

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:


Purchase Order (PO) Customer Flow

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:


Database Schema Changes Needed

New Tables

1. delivery_confirmations

create_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

2. pickup_requests

create_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

New Columns on Existing Tables

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

Implementation Phases

Phase 1: Fix Payment Authorization/Capture (Week 1)

Priority: CRITICAL - Blocking everything else

Step 1.1: Update Order Placement to Use auth_only

Files to modify:

Current (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

Step 1.2: Schedule CaptureAuthorizedPaymentsJob

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?

Step 1.3: Enhance CaptureAuthorizedPaymentsJob

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

Step 1.4: Testing

Estimated Time: 2-3 days

✅ Phase 1 Implementation Complete (October 4, 2025)

What Was Built:

  1. Payment Authorization on Checkout (app/services/authorize_net_payment_service.rb:257)

  2. Controller Updated (app/controllers/app/quotes_controller.rb:288)

  3. Automated Capture Scheduled (config/recurring.yml:32-35)

  4. Enhanced Capture Job (app/jobs/capture_authorized_payments_job.rb:33-56)

  5. Vendor Delivery Notification (app/mailers/vendor_mailer.rb)

  6. Database Migration (db/migrate/20251004150929_add_capture_tracking_to_orders.rb)

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)


Phase 2: Vendor Delivery Confirmation System ✅ COMPLETE

Priority: HIGH - Required for Ramp trigger Status: ✅ Implementation Complete (October 4, 2025)

✅ Phase 2 Implementation Complete

What Was Built:

  1. Vendor Availability System (NEW - Added per business request)

  2. Immediate Order Notifications (NEW - Added per business request)

  3. Delivery Confirmation Database

  4. Delivery Confirmation Email System

  5. Delivery Confirmation Portal & Public Endpoints

Portal URLs:

Fraud Detection Features:

Ready for Phase 4:


Phase 2: Original Implementation Plan (For Reference)

Priority: HIGH - Required for Ramp trigger

Step 2.1: Create Database Migrations

bin/rails generate migration CreateDeliveryConfirmations
bin/rails generate migration AddConfirmationFieldsToOrders

Step 2.2: Create DeliveryConfirmation Model

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

Step 2.3: Create SendDeliveryConfirmationEmailsJob

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

Step 2.4: Schedule the Job

File: config/recurring.yml

send_delivery_confirmations:
  class: SendDeliveryConfirmationEmailsJob
  queue: default
  schedule: at 8am every day  # Morning after delivery

Step 2.5: Create VendorMailer Methods

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

Step 2.6: Create Email Views

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>

Step 2.7: Create Vendors::ConfirmationsController

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

Step 2.8: Add Routes

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


Phase 3: Vendor Pickup/Removal Workflow (Week 4)

Priority: HIGH - Required for Ramp card cancellation

Step 3.1: Create PickupRequest Model

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

Step 3.2: Create Vendors::PickupRequestsController

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

Step 3.3: Create Admin::PickupRequestsController

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

Step 3.4: Create SendPickupConfirmationEmailsJob

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

Step 3.5: Schedule Pickup Confirmation Job

File: config/recurring.yml

send_pickup_confirmations:
  class: SendPickupConfirmationEmailsJob
  queue: default
  schedule: at 8am every day

Estimated Time: 4-5 days


Phase 4: Ramp Card Creation Workflow (Week 5-6)

Priority: MEDIUM - After phases 1-3 complete

This is where we finally integrate Ramp, but ONLY after all the prerequisites are done.

Step 4.1: Create CreateVendorPaymentCardJob

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

Step 4.2: Update RampCardService

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

Step 4.3: One-Time Card Viewing System

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

Step 4.4: Create Vendors::CardViewController

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


Flow Diagrams

Payment Capture Flow

┌─────────────────────────────────────────────────────────────┐
│ 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                  │
└─────────────────────────────────────────────────────────────┘

Vendor Delivery Confirmation Flow

┌─────────────────────────────────────────────────────────────┐
│ 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                      │
└─────────────────────────────────────────────────────────────┘

Ramp Card Creation & Viewing Flow

┌─────────────────────────────────────────────────────────────┐
│ 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)           │
└─────────────────────────────────────────────────────────────┘

Testing Strategy

Phase 1 Testing (Payment Capture)

# 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

Phase 2 Testing (Delivery Confirmation)

# 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

Phase 3 Testing (Pickup Workflow)

# 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

Phase 4 Testing (Ramp Integration)

# 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

Security Considerations

Payment Data (PCI Compliance)

Ramp Card Viewing

Fraud Prevention


Configuration Changes

config/recurring.yml

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

config/environments/production.rb

# 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
]

.env / Rails Credentials

# 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'] %>

Deployment Checklist

Before Phase 1 Deployment

Before Phase 2 Deployment

Before Phase 3 Deployment

Before Phase 4 Deployment (Ramp)


Success Metrics

Phase 1 (Payment Capture)

Phase 2 (Delivery Confirmation)

Phase 3 (Pickup Workflow)

Phase 4 (Ramp Integration)


Risk Mitigation

Payment Authorization Expiration

Risk: Authorizations expire after 30 days Mitigation:

Vendor Confirmation Fraud

Risk: Vendor confirms deliveries they didn't make Mitigation:

Ramp API Failures

Risk: Card creation fails, vendor can't get paid Mitigation:

One-Time View Token Abuse

Risk: Vendor shares token, multiple people view card Mitigation:


Open Questions & Decisions Needed

  1. Authorization Hold Period:

  2. PO Customer Invoicing:

  3. Vendor Onboarding:

  4. Recurring Card Termination:

  5. Overage Handling:


Next Steps

  1. Review this document with team - Get buy-in on approach
  2. Prioritize phases - Can we delay Phase 3 and do Phase 2 + 4?
  3. Assign developers - Who builds what?
  4. Set timeline - Realistic dates for each phase
  5. Staging environment - Set up Authorize.Net sandbox for testing
  6. Start Phase 1 - Fix auth/capture flow ASAP

Document Status: DRAFT - Awaiting Review Next Review Date: TBD Owner: Development Team