Skip to content

Instantly share code, notes, and snippets.

@jsqu99
Created May 26, 2015 15:58
Show Gist options
  • Save jsqu99/f0735b88156dd52436df to your computer and use it in GitHub Desktop.
Save jsqu99/f0735b88156dd52436df to your computer and use it in GitHub Desktop.

Revisions

  1. jsqu99 created this gist May 26, 2015.
    227 changes: 227 additions & 0 deletions gistfile1.txt
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,227 @@
    require 'active_support/core_ext/date/calculations'
    require 'active_support/core_ext/numeric/time'
    require 'base64'

    #Unirest.timeout(240) # generous timeout

    class ShipstationClient
    class ResponseError < StandardError; end

    class << self
    def request(method, path, options)

    response = Unirest.send method, "https://ssapi.shipstation.com/#{path}", options

    return response if response.code == 200

    raise ResponseError, "#{response.code}, API error: #{response.body.inspect}"
    end
    end
    end

    class ShipStationApp
    def initialize(config)
    @config = config
    end
    # ShipStation appears to be recording their timestamps in local (PST) time but storing that timestamp
    # as UTC (so it's basically 7-8 hours off the correct time (depending on daylight savings). To compensate
    # for this the timestamp we use for "since" should be adjusted accordingly.
    ZONE = "Pacific Time (US & Canada)"

    # REST API doc https://www.mashape.com/shipstation/shipstation

    def add_shipment(shipment)
    # Shipstation wants orders and then it creates shipments. This integration assumes the
    # shipments are already split and will just create "orders" that are identical to the
    # storefront concept of a shipment.

    order = populate_order(shipment)
    options = {
    headers: ship_headers.merge("content-type" => "application/json"),
    parameters: order.to_json
    }

    response = ShipstationClient.request :post, "Orders/CreateOrder", options
    puts "result 200 - Shipment transmitted to ShipStation: #{response.body['orderId']}"
    end

    private

    def map_carrier(carrier_name)
    response = ShipstationClient.request :get, "Carriers", headers: ship_headers

    response.body.each do |carrier|
    return carrier["code"] if carrier["name"] == carrier_name
    end

    raise "There is no carrier named '#{carrier_name}' configured with this ShipStation account"
    end

    def map_service(carrier_code, service_name)
    response = ShipstationClient.request :get, "Carriers/ListServices?carrierCode=#{carrier_code}", headers: ship_headers

    response.body.each do |service|
    return service["code"] if service["name"] == service_name
    end

    raise "There is no service named '#{service_name}' associated with the carrier_code of '#{carrier_code}'"
    end

    def map_package(carrier_code, package_name)
    response = ShipstationClient.request :get, "Carriers/ListPackages?carrierCode=#{carrier_code}", headers: ship_headers

    response.body.each do |package|
    return package["code"] if package["name"] == package_name
    end

    raise "There is no package named '#{package_name}' associated with the carrier_code of '#{carrier_code}'"
    end

    def populate_order(shipment)
    order = {
    "customerEmail" => shipment[:email],
    "customerUsername" => shipment[:email],
    "orderNumber" => shipment[:id], #required
    "orderDate" => shipment[:created_at] || Time.now,
    "paymentDate" => shipment[:created_at] || Time.now,
    "orderStatus" => map_status(shipment[:status]), #required: hold, canceled, awaiting_shipment
    "shipTo" => populate_address(shipment[:shipping_address]), #required (see populate_address for details)
    "billTo" => populate_address(shipment[:billing_address]) || populate_address(shipment[:shipping_address]),
    "customerNotes" => shipment[:delivery_instructions],
    "internalNotes" => shipment[:internal_notes],
    "gift" => shipment[:is_gift],
    "packageCode" => 'package',
    "advancedOptions" => populate_advanced(shipment),
    "items" => populate_items(shipment[:items])
    }

    if shipment[:amount_paid]
    order["amountPaid"] = shipment[:amount_paid].to_f.to_s
    end

    if shipment[:shipping_amount]
    order["shippingAmount"] = shipment[:shipping_amount].to_f.to_s
    end

    if shipment[:tax_amount]
    order["taxAmount"] = shipment[:tax_amount].to_f.to_s
    end

    if shipment[:gift_message]
    order["giftMessage"] = shipment[:gift_message]
    end

    if shipment[:confirmation]
    order["confirmation"] = map_confirmation(shipment[:confirmation])
    end

    if shipment[:requested_shipping_service]
    order["requestedShippingService"] = shipment[:requested_shipping_service]
    elsif shipment[:shipping_carrier]
    carrier_code = map_carrier(shipment[:shipping_carrier])
    order["carrierCode"] = carrier_code
    order["serviceCode"] = map_service(carrier_code, shipment[:shipping_method])
    order["packageCode"] = map_package(carrier_code, shipment[:package]) if shipment[:package]
    end

    order
    end

    def populate_advanced(shipment)
    advanced = {
    "storeId" => @config[:shipstation_store_id],
    "customfield1" => shipment[:custom_field_1],
    "customfield2" => shipment[:custom_field_2],
    "customfield3" => shipment[:custom_field_3],
    }

    if shipment[:contains_alcohol]
    advanced["containsAlcohol"] = shipment[:contains_alcohol]
    end

    if shipment[:saturday_delivery]
    advanced["saturdayDelivery"] = shipment[:saturday_delivery]
    end

    if shipment[:non_machinable]
    advanced["nonMachinable"] = shipment[:non_machinable]
    end

    advanced
    end

    def populate_items(line_items)
    return if line_items.nil?
    line_items.map do |item|
    {
    "lineItemKey" => nil,
    "sku" => item[:product_id],
    "name" => item[:name],
    "imageUrl" => item[:image_url],
    "quantity" => item[:quantity],
    "unitPrice" => item[:price]
    }
    end
    end

    def populate_address(address)
    return if address.nil? || address.empty?
    address_hash = {
    :name => "#{address[:firstname]} #{address[:lastname]}", #required
    :street1 => address[:address1], #required
    :street2 => address[:address2],
    :street3 => address[:address3],
    :city => address[:city], #required
    :state => address[:state], #required
    :postalCode => address[:zipcode], #required
    :country => address[:country], #required
    :phone => address[:phone],
    :company => address[:company]
    }
    if address[:is_residential].present?
    address_hash[:residential] = address[:is_residential]
    end

    address_hash
    end

    def map_status(status)
    case status
    when 'hold'
    'on_hold'
    when 'canceled', 'cancelled'
    'cancelled'
    else
    'awaiting_shipment'
    end
    end

    def map_confirmation(confirmation)
    case confirmation
    when 'Delivery'
    'delivery'
    when 'Signature'
    'signature'
    when 'Adult Signature'
    'adult_signature'
    end
    end

    def ship_headers
    # the parameter authorization is for old customers - legacy support (Mashape)
    # new customers should use key & secret
    if !@config[:key].to_s.empty? && !@config[:secret].to_s.empty?
    authorization = Base64.strict_encode64("#{@config[:key]}:#{@config[:secret]}")
    else
    authorization = @config[:authorization]
    end

    headers = { 'Authorization' => "Basic #{authorization}" }

    unless @config[:x_partner].to_s.empty?
    headers['x-partner'] = @config[:x_partner]
    end

    headers
    end
    end