module ActiveShipping
class ExternalReturnLabelRequest
CAP_STRING_LEN = 100
USPS_EMAIL_REGEX = /^([a-zA-Z0-9_\-\.]+)@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.)|(([a-zA-Z0-9\-]+\.)+))([a-zA-Z]{2,4}|[0-9]{1,3})(\]?)$/
LABEL_FORMAT = {
'Instructions' => 'null',
'No Instructions' => 'NOI',
'Double Label' => 'TWO'
}
SERVICE_TYPE_CODE = [
'044', '019', '596', '020', '597','022', '024', '017', '018'
]
CALL_CENTER_OR_SELF_SERVICE = ['CallCenter', 'Customer']
LABEL_DEFINITION = ['4X6', 'Zebra-4X6', '4X4', '3X6']
IMAGE_TYPE = ['PDF', 'TIF']
attr_reader :customer_name,
:customer_address1,
:customer_address2,
:customer_city,
:customer_state,
:customer_zipcode,
:customer_urbanization,
:company_name,
:attention,
:label_format,
:label_definition,
:service_type_code,
:merchandise_description,
:insurance_amount,
:address_override_notification,
:packaging_information,
:packaging_information2,
:call_center_or_self_service,
:image_type,
:address_validation,
:sender_name,
:sender_email,
:recipient_name,
:recipient_email,
:recipient_bcc,
:merchant_account_id,
:mid
def initialize(options = {})
options.each do |pair|
self.public_send("#{pair[0]}=".to_sym, pair[1]) if self.respond_to?("#{pair[0]}=".to_sym)
end
verify_or_raise_required
end
def self.from_hash(options={})
self.new(options)
end
# Sent by the system containing the returns label attachment and message.
def recipient_bcc=(v)
@recipient_bcc = validate_email(v, __method__)
end
# Sent by the system containing the returns label attachment and message.
# Optional.
def recipient_email=(v)
@recipient_email = validate_email(v, __method__)
end
# The name in an email sent by the system containing the returns label attachment.
# Optional.
def recipient_name=(v)
@recipient_name = nil
if (v = sanitize(v)) && v.length > 0
@recipient_name = v
else
raise USPSValidationError, "'#{v}' is not a valid string in #{__method__}"
end
end
# The From address in an email sent by the system containing the returns
# label attachment and message, Defaults to DONOTREPLY@USPSReturns.com
# if a recipient email is entered and a sender email is not.
# Optional.
def sender_email=(v)
@sender_email = validate_email(v, __method__)
end
# The From name in an email sent by the system containing the returns
# label attachment. Defaults to “Merchant Returns” if a recipient name
# is entered and a sender name is not.
# Optional.
def sender_name=(v)
@sender_name = nil
if (v = sanitize(v)) && v.length > 0
@sender_name = v
else
raise USPSValidationError, "'#{v}' is not a valid string in #{__method__}"
end
end
# Used to override the validation of the customer address.
# If true, the address will be validated against WebTools.
# If false, the system will bypass the validation.
# Optional.
def address_validation=(v)
@address_validation = to_bool(v, true)
end
# Used to select the format of the return label.
# Optional.
# * PDF Default.
# * TIF
def image_type=(v)
@image_type = validate_set_inclusion(v.to_s.upcase, IMAGE_TYPE, __method__)
end
# Used to determine if the returns label request is coming from a
# merchant call center agent or an end customer.
# Required.
# [CallCenter]
# [Customer]
def call_center_or_self_service=(v)
@call_center_or_self_service = validate_set_inclusion(v, CALL_CENTER_OR_SELF_SERVICE, __method__)
end
# Package information can be one of three types: RMA, Invoice or
# Order number. This will appear on the second label generated when
# the LabelFormat “TWO” is selected.
# Optional.
def packaging_information2=(v)
@packaging_information2 = validate_string_length(v, 15, __method__)
end
# Package information can be one of three types: RMA, Invoice or
# Order number. This will appear on the generated label.
# Optional.
def packaging_information=(v)
@packaging_information = validate_string_length(v, 15, __method__)
end
# Override address if more address information
# is needed or system cannot find address. If
# the address_override_notification value is
# true then any address error being passed from
# WebTools would be bypassed and a successful
# response will be sent.
# Required.
def address_override_notification=(v)
@address_validation = to_bool(v)
end
# Insured amount of package.
def insurance_amount=(v)
@insurance_amount = nil
if (1..200).include?(v.to_f)
@insurance_amount = v
else
raise USPSValidationError, "#{__method__} must be a numerical value between 1 and 200, found value '#{v}'."
end
end
# Description of the merchandise.
# Optional.
def merchandise_description=(v)
@merchandise_description = validate_string_length(v, 255, __method__)
end
# Service type of the label as specified in the merchant profile setup.
# Required.
# [044] (Parcel Return Service)
# [019] (Priority Mail Returns service)
# [596] (Priority Mail Returns service, Insurance <= $200)
# [020] (First-Class Package Return service)
# [597] (First-Class Package Return service, Insurance <= $200)
# [022] (Ground Return Service)
# [024] (PRS – Full Network)
# [017] (PRS – Full Network, Insurance <=$200)
# [018] (PRS – Full Network, Insurance >$200)
def service_type_code=(v)
@service_type_code = validate_set_inclusion(v, SERVICE_TYPE_CODE, __method__)
end
# Size of the label.
# Required.
# * 4X6
# * Zebra-4X6
# * 4X4
# * 3X6
def label_definition=(v)
@label_definition = validate_set_inclusion(v, LABEL_DEFINITION, __method__)
end
def label_format
@label_format && LABEL_FORMAT[@label_format]
end
# Format in which the label(s) will be printed.
# * null (“Instructions”)
# * NOI (“No Instructions”)
# * TWO (“Double Label”)
def label_format=(v)
@label_format = validate_set_inclusion(v, LABEL_FORMAT.keys, __method__)
end
# The intended recipient of the returned package (e.g. Returns Department).
# Optional.
def attention=(v)
@attention = validate_string_length(v, 38, __method__)
end
# The name of the company to which the package is being returned.
# Optional.
def company_name=(v)
@company_name = validate_string_length(v, 38, __method__)
end
# Required.
def merchant_account_id=(v)
@merchant_account_id = nil
if v.to_i > 0
@merchant_account_id = v
else
raise USPSValidationError, "#{__method__} must be a valid positive integer, found value '#{v}'."
end
end
# Required.
def mid=(v)
@mid = nil
if v.to_s =~ /^\d{6,9}$/
@mid = v
else
raise USPSValidationError, "#{__method__} must be a valid integer between 6 and 9 digits in length, found value '#{v}'."
end
end
# Urbanization of customer returning the package (only applicable to Puerto Rico addresses).
# Optional.
def customer_urbanization=(v)
@customer_urbanization = validate_string_length(v, 32, __method__)
end
# Name of customer returning package.
# Required.
def customer_name=(v)
@customer_name = validate_range(v, 1, 32, __method__)
end
# Address of the customer returning the package.
# Required.
def customer_address1=(v)
@customer_address1 = validate_range(v, 1, 32, __method__)
end
# Secondary address unit designator / number of customer
# returning the package. (such as an apartment or
# suite number, e.g. APT 202, STE 100)
def customer_address2=(v)
@customer_address2 = validate_range(v, 0, 32, __method__)
end
# City of customer returning the package.
# Required.
def customer_city=(v)
@customer_city = validate_range(v, 1, 32, __method__)
end
# State of customer returning the package.
# Required.
def customer_state=(v)
@customer_state = nil
if (v = sanitize(v)) && v =~ /^[a-zA-Z]{2}$/
@customer_state = v
else
raise USPSValidationError, "#{__method__} must be a String 2 chars in length, found value '#{v}'."
end
end
# Zipcode of customer returning the package.
# According to the USPS documentation, Zipcode is optional
# unless address_override_notification is true
# and address_validation is set to false.
# It's probably just easier to require Zipcodes.
# Required.
def customer_zipcode=(v)
@customer_zipcode = nil
if (v = sanitize(v))
v = v[0..4]
if v =~ /^\d{5}$/
@customer_zipcode = v
end
else
raise USPSValidationError, "#{__method__} must be a 5 digit number, found value '#{v}'."
end
end
def verify_or_raise_required
%w(customer_name customer_address1 customer_city customer_state
customer_zipcode label_format label_definition service_type_code
call_center_or_self_service).each do |attr|
raise USPSMissingRequiredTagError.new(attr.camelize, attr) unless send(attr.to_sym)
end
# Safer than using inflection acroynms
raise USPSMissingRequiredTagError.new("MID", "mid") unless mid
raise USPSMissingRequiredTagError.new("MerchantAccountID", "merchant_account_id") unless merchant_account_id
end
def to_xml
xml_builder = Nokogiri::XML::Builder.new do |xml|
xml.ExternalReturnLabelRequest do
xml.CustomerName { xml.text(customer_name) }
xml.CustomerAddress1 { xml.text(customer_address1) }
xml.CustomerAddress2 { xml.text(customer_address2) } if customer_address2
xml.CustomerCity { xml.text(customer_city) }
xml.CustomerState { xml.text(customer_state) }
xml.CustomerZipCode { xml.text(customer_zipcode) } if customer_zipcode
xml.CustomerUrbanization { xml.text(customer_urbanization) } if customer_urbanization
xml.MerchantAccountID { xml.text(merchant_account_id) }
xml.MID { xml.text(mid) }
xml.SenderName { xml.text(sender_name) } if sender_name
xml.SenderEmail { xml.text(sender_email) } if sender_email
xml.RecipientName { xml.text(recipient_name) } if recipient_name
xml.RecipientEmail { xml.text(recipient_email) } if recipient_email
xml.RecipientBcc { xml.text(recipient_bcc) } if recipient_bcc
xml.LabelFormat { xml.text(label_format) } if label_format
xml.LabelDefinition { xml.text(label_definition) } if label_definition
xml.ServiceTypeCode { xml.text(service_type_code) } if service_type_code
xml.CompanyName { xml.text(company_name) } if company_name
xml.Attention { xml.text(attention) } if attention
xml.CallCenterOrSelfService { xml.text(call_center_or_self_service) }
xml.MerchandiseDescription { xml.text(merchandise_description) } if merchandise_description
xml.InsuranceAmount { xml.text(insurance_amount) } if insurance_amount
xml.AddressOverrideNotification { xml.text(!!address_override_notification) }
xml.PackageInformation { xml.text(packaging_information) } if packaging_information
xml.PackageInformation2 { xml.text(packaging_information2) } if packaging_information2
xml.ImageType { xml.text(image_type) } if image_type
xml.AddressValidation { xml.text(!!address_validation) }
end
end
xml_builder.to_xml
end
private
def to_bool(v, default = false)
v = v.to_s
if v =~ (/(true|yes|1)$/i)
true
elsif v =~ (/(false|no|0)$/i)
false
else
default
end
end
def sanitize(v)
if v.is_a?(String)
v.strip!
v[0..CAP_STRING_LEN - 1]
else
nil
end
end
def validate_range(v, min, max, meth)
if (v = sanitize(v).to_s) && ((min.to_i)..(max.to_i)).include?(v.length)
if v.length == 0
nil
else
v
end
else
raise USPSValidationError, "#{meth} must be a String between #{min.to_i} and #{max.to_i} chars in length, found value '#{v}'."
end
end
def validate_string_length(s, max_len, meth)
if (s = sanitize(s)) && s.length <= max_len.to_i
s
else
raise USPSValidationError, "#{meth} must be a String no more than #{max_len} chars in length, found value '#{s}'."
end
end
def validate_set_inclusion(v, set, meth)
if set.respond_to?(:include?) && set.include?(v)
v
else
raise USPSValidationError, "#{v} is not valid in #{meth}, try any of the following: #{(set.respond_to?(:join) && set.join(',')) || ''}"
end
end
def validate_email(v, meth)
if (v = sanitize(v)) && v =~ USPS_EMAIL_REGEX
v
else
raise USPSValidationError, "'#{v}' is not a valid e-mail in #{meth}"
end
end
end
end