deliveries 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.github/workflows/main.yml +19 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +34 -0
- data/CHANGELOG.md +13 -0
- data/Gemfile +12 -0
- data/Gemfile.lock +127 -0
- data/LICENSE.txt +21 -0
- data/README.md +207 -0
- data/Rakefile +12 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/deliveries.gemspec +36 -0
- data/lib/deliveries/address.rb +71 -0
- data/lib/deliveries/checkpoint.rb +12 -0
- data/lib/deliveries/collection_point.rb +45 -0
- data/lib/deliveries/courier.rb +69 -0
- data/lib/deliveries/couriers/correos_express/address.rb +31 -0
- data/lib/deliveries/couriers/correos_express/collection_points/search/format_response.rb +82 -0
- data/lib/deliveries/couriers/correos_express/collection_points/search.rb +56 -0
- data/lib/deliveries/couriers/correos_express/labels/generate.rb +68 -0
- data/lib/deliveries/couriers/correos_express/pickups/create/defaults.rb +60 -0
- data/lib/deliveries/couriers/correos_express/pickups/create/format_params.rb +95 -0
- data/lib/deliveries/couriers/correos_express/pickups/create.rb +66 -0
- data/lib/deliveries/couriers/correos_express/pickups/cutoff_time/format_params.rb +40 -0
- data/lib/deliveries/couriers/correos_express/pickups/cutoff_time.rb +61 -0
- data/lib/deliveries/couriers/correos_express/pickups/trace/correos.test.wsdl +163 -0
- data/lib/deliveries/couriers/correos_express/pickups/trace/correos.wsdl +163 -0
- data/lib/deliveries/couriers/correos_express/pickups/trace/format_response.rb +71 -0
- data/lib/deliveries/couriers/correos_express/pickups/trace.rb +49 -0
- data/lib/deliveries/couriers/correos_express/shipments/create/defaults.rb +80 -0
- data/lib/deliveries/couriers/correos_express/shipments/create/format_params.rb +99 -0
- data/lib/deliveries/couriers/correos_express/shipments/create.rb +101 -0
- data/lib/deliveries/couriers/correos_express/shipments/trace/format_response.rb +67 -0
- data/lib/deliveries/couriers/correos_express/shipments/trace.rb +65 -0
- data/lib/deliveries/couriers/correos_express.rb +162 -0
- data/lib/deliveries/couriers/dummy.rb +106 -0
- data/lib/deliveries/couriers/mondial_relay/address.rb +31 -0
- data/lib/deliveries/couriers/mondial_relay/collection_points/search/format_response.rb +103 -0
- data/lib/deliveries/couriers/mondial_relay/labels/generate.rb +40 -0
- data/lib/deliveries/couriers/mondial_relay/pickups/create/format_params.rb +57 -0
- data/lib/deliveries/couriers/mondial_relay/shipments/create/defaults.rb +96 -0
- data/lib/deliveries/couriers/mondial_relay/shipments/create/format_params.rb +68 -0
- data/lib/deliveries/couriers/mondial_relay/shipments/create.rb +36 -0
- data/lib/deliveries/couriers/mondial_relay/shipments/trace/format_response.rb +94 -0
- data/lib/deliveries/couriers/mondial_relay/shipments/trace.rb +47 -0
- data/lib/deliveries/couriers/mondial_relay/status_codes.rb +105 -0
- data/lib/deliveries/couriers/mondial_relay.rb +189 -0
- data/lib/deliveries/couriers/mondial_relay_dual/address.rb +31 -0
- data/lib/deliveries/couriers/mondial_relay_dual/pickups/create/format_params.rb +39 -0
- data/lib/deliveries/couriers/mondial_relay_dual/shipments/create/format_params.rb +101 -0
- data/lib/deliveries/couriers/mondial_relay_dual/shipments/create.rb +133 -0
- data/lib/deliveries/couriers/mondial_relay_dual.rb +105 -0
- data/lib/deliveries/couriers/spring/address.rb +23 -0
- data/lib/deliveries/couriers/spring/labels/generate.rb +33 -0
- data/lib/deliveries/couriers/spring/request.rb +43 -0
- data/lib/deliveries/couriers/spring/shipments/create/defaults.rb +73 -0
- data/lib/deliveries/couriers/spring/shipments/create/format_params.rb +77 -0
- data/lib/deliveries/couriers/spring/shipments/create.rb +45 -0
- data/lib/deliveries/couriers/spring/shipments/trace/format_response.rb +79 -0
- data/lib/deliveries/couriers/spring/shipments/trace.rb +29 -0
- data/lib/deliveries/couriers/spring.rb +84 -0
- data/lib/deliveries/couriers/ups/collection_points/search.rb +142 -0
- data/lib/deliveries/couriers/ups/json_request.rb +40 -0
- data/lib/deliveries/couriers/ups/labels/generate.rb +72 -0
- data/lib/deliveries/couriers/ups/shipments/create.rb +210 -0
- data/lib/deliveries/couriers/ups/shipments/trace.rb +79 -0
- data/lib/deliveries/couriers/ups.rb +122 -0
- data/lib/deliveries/couriers.rb +14 -0
- data/lib/deliveries/delivery.rb +20 -0
- data/lib/deliveries/errors.rb +22 -0
- data/lib/deliveries/label.rb +20 -0
- data/lib/deliveries/label_utils.rb +67 -0
- data/lib/deliveries/labels.rb +40 -0
- data/lib/deliveries/pickup.rb +15 -0
- data/lib/deliveries/shipment.rb +15 -0
- data/lib/deliveries/tracking_info.rb +29 -0
- data/lib/deliveries/version.rb +5 -0
- data/lib/deliveries.rb +63 -0
- metadata +240 -0
@@ -0,0 +1,43 @@
|
|
1
|
+
module Deliveries
|
2
|
+
module Couriers
|
3
|
+
module Spring
|
4
|
+
module Request
|
5
|
+
module_function
|
6
|
+
|
7
|
+
def execute(params:)
|
8
|
+
response = HTTParty.post(
|
9
|
+
endpoint,
|
10
|
+
body: params.to_json,
|
11
|
+
headers: headers,
|
12
|
+
debug_output: Deliveries.debug ? Deliveries.logger : nil
|
13
|
+
)
|
14
|
+
raise Deliveries::ClientError unless response.success?
|
15
|
+
|
16
|
+
parsed_response = JSON.parse(response.body, symbolize_names: true)
|
17
|
+
error_level = parsed_response[:ErrorLevel]
|
18
|
+
|
19
|
+
if error_level.zero?
|
20
|
+
parsed_response
|
21
|
+
else
|
22
|
+
raise Deliveries::APIError.new(
|
23
|
+
parsed_response[:Error],
|
24
|
+
error_level
|
25
|
+
)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def headers
|
30
|
+
{ 'Content-Type' => "text/json; charset='UTF-8'" }
|
31
|
+
end
|
32
|
+
|
33
|
+
def endpoint
|
34
|
+
if Spring.live?
|
35
|
+
Spring::ENDPOINT_LIVE
|
36
|
+
else
|
37
|
+
Spring::ENDPOINT_TEST
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Deliveries
|
2
|
+
module Couriers
|
3
|
+
module Spring
|
4
|
+
module Shipments
|
5
|
+
class Create
|
6
|
+
module Defaults
|
7
|
+
PARAMS = {
|
8
|
+
Apikey: '',
|
9
|
+
Command: 'OrderShipment',
|
10
|
+
Shipment: {
|
11
|
+
LabelFormat: 'PDF',
|
12
|
+
ShipperReference: '',
|
13
|
+
DisplayId: '',
|
14
|
+
InvoiceNumber: '',
|
15
|
+
Service: '',
|
16
|
+
ConsignorAddress: {
|
17
|
+
Name: '',
|
18
|
+
Company: '',
|
19
|
+
AddressLine1: '',
|
20
|
+
AddressLine2: '',
|
21
|
+
AddressLine3: '',
|
22
|
+
City: '',
|
23
|
+
State: '',
|
24
|
+
Zip: '',
|
25
|
+
Country: '',
|
26
|
+
Phone: '',
|
27
|
+
Email: '',
|
28
|
+
Vat: ''
|
29
|
+
},
|
30
|
+
ConsigneeAddress: {
|
31
|
+
Name: '',
|
32
|
+
Company: '',
|
33
|
+
AddressLine1: '',
|
34
|
+
AddressLine2: '',
|
35
|
+
AddressLine3: '',
|
36
|
+
City: '',
|
37
|
+
State: '',
|
38
|
+
Zip: '',
|
39
|
+
Country: '',
|
40
|
+
Phone: '',
|
41
|
+
Email: '',
|
42
|
+
Vat: ''
|
43
|
+
},
|
44
|
+
Weight: '1',
|
45
|
+
WeightUnit: 'kg',
|
46
|
+
Length: '',
|
47
|
+
Width: '',
|
48
|
+
Height: '',
|
49
|
+
DimUnit: '',
|
50
|
+
Value: '',
|
51
|
+
Currency: 'EUR',
|
52
|
+
CustomsDuty: 'DDU',
|
53
|
+
Description: '',
|
54
|
+
DeclarationType: 'SaleOfGoods',
|
55
|
+
Products: [
|
56
|
+
{
|
57
|
+
Description: '',
|
58
|
+
Sku: '',
|
59
|
+
HsCode: '',
|
60
|
+
OriginCountry: '',
|
61
|
+
PurchaseUrl: '',
|
62
|
+
Quantity: '',
|
63
|
+
Value: ''
|
64
|
+
}
|
65
|
+
]
|
66
|
+
}
|
67
|
+
}.freeze
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,77 @@
|
|
1
|
+
module Deliveries
|
2
|
+
module Couriers
|
3
|
+
module Spring
|
4
|
+
module Shipments
|
5
|
+
class Create
|
6
|
+
class FormatParams
|
7
|
+
attr_accessor :sender, :receiver, :parcels, :reference_code
|
8
|
+
|
9
|
+
def initialize(sender:, receiver:, parcels:, reference_code:)
|
10
|
+
self.sender = sender
|
11
|
+
self.receiver = receiver
|
12
|
+
self.parcels = parcels
|
13
|
+
self.reference_code = reference_code
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute
|
17
|
+
shipment = {
|
18
|
+
ShipperReference: reference_code,
|
19
|
+
Service: Deliveries::Couriers::Spring.config("countries.#{receiver.country.downcase.to_sym}.service")
|
20
|
+
}
|
21
|
+
|
22
|
+
# Consignor address (from).
|
23
|
+
shipment[:ConsignorAddress] = {
|
24
|
+
Name: sender.name,
|
25
|
+
Company: sender.name,
|
26
|
+
AddressLine1: sender.street,
|
27
|
+
AddressLine2: sender.street2,
|
28
|
+
AddressLine3: sender.street3,
|
29
|
+
City: sender.city,
|
30
|
+
State: sender.state,
|
31
|
+
Zip: sender.postcode,
|
32
|
+
Country: sender.country,
|
33
|
+
Phone: sender.phone,
|
34
|
+
Email: sender.email
|
35
|
+
}
|
36
|
+
|
37
|
+
# Consignee address (to).
|
38
|
+
shipment[:ConsigneeAddress] = {
|
39
|
+
Name: receiver.name,
|
40
|
+
Company: '',
|
41
|
+
AddressLine1: receiver.street,
|
42
|
+
AddressLine2: receiver.street2,
|
43
|
+
AddressLine3: receiver.street3,
|
44
|
+
City: receiver.city,
|
45
|
+
State: receiver.state,
|
46
|
+
Zip: receiver.postcode,
|
47
|
+
Country: receiver.country,
|
48
|
+
Phone: receiver.phone,
|
49
|
+
Email: receiver.email
|
50
|
+
}
|
51
|
+
|
52
|
+
# Products collection.
|
53
|
+
shipment[:Products] = 1.upto(parcels).map do
|
54
|
+
{
|
55
|
+
Description: Deliveries::Couriers::Spring.config('default_product.description'),
|
56
|
+
Sku: '',
|
57
|
+
HsCode: Deliveries::Couriers::Spring.config('default_product.hs_code'),
|
58
|
+
OriginCountry: Deliveries::Couriers::Spring.config('default_product.origin_country'),
|
59
|
+
PurchaseUrl: '',
|
60
|
+
Quantity: Deliveries::Couriers::Spring.config('default_product.quantity'),
|
61
|
+
Value: Deliveries::Couriers::Spring.config('default_product.value')
|
62
|
+
}
|
63
|
+
end
|
64
|
+
|
65
|
+
params = {
|
66
|
+
Apikey: Deliveries::Couriers::Spring.config(:api_key),
|
67
|
+
Shipment: shipment
|
68
|
+
}
|
69
|
+
|
70
|
+
Defaults::PARAMS.deep_merge(params)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module Deliveries
|
2
|
+
module Couriers
|
3
|
+
module Spring
|
4
|
+
module Shipments
|
5
|
+
class Create
|
6
|
+
attr_accessor :sender, :receiver, :parcels, :reference_code
|
7
|
+
|
8
|
+
def initialize(sender:, receiver:, parcels:, reference_code:)
|
9
|
+
self.sender = sender
|
10
|
+
self.receiver = receiver
|
11
|
+
self.parcels = parcels
|
12
|
+
self.reference_code = reference_code
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute
|
16
|
+
params = Deliveries::Couriers::Spring::Shipments::Create::FormatParams.new(
|
17
|
+
sender: sender.courierize(:spring),
|
18
|
+
receiver: receiver.courierize(:spring),
|
19
|
+
parcels: parcels,
|
20
|
+
reference_code: reference_code
|
21
|
+
).execute
|
22
|
+
|
23
|
+
response = Deliveries::Couriers::Spring::Request.execute(params: params)
|
24
|
+
|
25
|
+
tracking_code = response[:Shipment][:TrackingNumber]
|
26
|
+
label = Label.new(
|
27
|
+
url: response[:Shipment][:CarrierTrackingUrl],
|
28
|
+
raw: Base64.decode64(response[:Shipment][:LabelImage]).force_encoding('binary')
|
29
|
+
)
|
30
|
+
|
31
|
+
Deliveries::Delivery.new(
|
32
|
+
courier_id: Deliveries::Couriers::Spring::COURIER_ID,
|
33
|
+
sender: sender,
|
34
|
+
receiver: receiver,
|
35
|
+
parcels: parcels,
|
36
|
+
reference_code: reference_code,
|
37
|
+
tracking_code: tracking_code,
|
38
|
+
label: label
|
39
|
+
)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
require 'active_support/time'
|
2
|
+
|
3
|
+
module Deliveries
|
4
|
+
module Couriers
|
5
|
+
module Spring
|
6
|
+
module Shipments
|
7
|
+
class Trace
|
8
|
+
class FormatResponse
|
9
|
+
attr_accessor :response
|
10
|
+
|
11
|
+
def initialize(response:)
|
12
|
+
self.response = response
|
13
|
+
end
|
14
|
+
|
15
|
+
def execute
|
16
|
+
checkpoints = response[:Events].sort_by { |event| event[:DateTime] }
|
17
|
+
|
18
|
+
tracking_info_params = {}
|
19
|
+
tracking_info_params[:courier_id] = Deliveries::Couriers::Spring::COURIER_ID
|
20
|
+
tracking_info_params[:tracking_code] = response[:TrackingNumber]
|
21
|
+
tracking_info_params[:status] = status(checkpoints.last[:Code])
|
22
|
+
tracking_info_params[:checkpoints] = formatted_checkpoints(checkpoints)
|
23
|
+
|
24
|
+
tracking_info_params
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def formatted_checkpoints(shipment_statuses)
|
30
|
+
shipment_statuses.map do |shipment_status|
|
31
|
+
formatted_checkpoint(shipment_status)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def formatted_checkpoint(shipment_status)
|
36
|
+
Deliveries::Checkpoint.new(
|
37
|
+
status: status(shipment_status[:Code]),
|
38
|
+
location: shipment_status[:Country],
|
39
|
+
tracked_at: Time.zone.strptime(shipment_status[:DateTime], '%Y-%m-%d %H:%M:%S'),
|
40
|
+
description: shipment_status[:CarrierDescription]
|
41
|
+
)
|
42
|
+
end
|
43
|
+
|
44
|
+
def status(code)
|
45
|
+
# Spring Tracking Event Codes List
|
46
|
+
# ---
|
47
|
+
# 0 - PARCEL CREATED
|
48
|
+
# 20 - ACCEPTED
|
49
|
+
# 21 - IN TRANSIT
|
50
|
+
# 31 - DELIVERY EXCEPTION
|
51
|
+
# 40 - IN CUSTOMS
|
52
|
+
# 41 - CUSTOMS EXCEPTION
|
53
|
+
# 91 - DELIVERY ATTEMPTED
|
54
|
+
# 92 - DELIVERY AWAITING COLLECTION
|
55
|
+
# 93 - DELIVERY SCHEDULED
|
56
|
+
# 100 - DELIVERED
|
57
|
+
# 111 - LOST OR DESTROYED
|
58
|
+
# 124 - RETURN IN TRANSIT
|
59
|
+
# 125 - RETURN RECEIVED
|
60
|
+
|
61
|
+
case code
|
62
|
+
when 0, 20
|
63
|
+
:registered
|
64
|
+
when 21, 40, 41, 91, 93
|
65
|
+
:in_transit
|
66
|
+
when 92
|
67
|
+
:in_collection_point
|
68
|
+
when 100
|
69
|
+
:delivered
|
70
|
+
else
|
71
|
+
:unknown_status
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Deliveries
|
2
|
+
module Couriers
|
3
|
+
module Spring
|
4
|
+
module Shipments
|
5
|
+
class Trace
|
6
|
+
attr_accessor :tracking_code
|
7
|
+
|
8
|
+
def initialize(tracking_code:)
|
9
|
+
self.tracking_code = tracking_code
|
10
|
+
end
|
11
|
+
|
12
|
+
def execute
|
13
|
+
params = {
|
14
|
+
Apikey: Deliveries::Couriers::Spring.config(:api_key),
|
15
|
+
Command: 'TrackShipment',
|
16
|
+
Shipment: {
|
17
|
+
TrackingNumber: tracking_code
|
18
|
+
}
|
19
|
+
}
|
20
|
+
|
21
|
+
response = Deliveries::Couriers::Spring::Request.execute(params: params)
|
22
|
+
|
23
|
+
response[:Shipment]
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
require_relative 'spring/shipments/trace/format_response'
|
2
|
+
require_relative 'spring/shipments/trace'
|
3
|
+
require_relative 'spring/shipments/create/defaults'
|
4
|
+
require_relative 'spring/shipments/create/format_params'
|
5
|
+
require_relative 'spring/shipments/create'
|
6
|
+
require_relative 'spring/labels/generate'
|
7
|
+
require_relative 'spring/address'
|
8
|
+
require_relative 'spring/request'
|
9
|
+
|
10
|
+
module Deliveries
|
11
|
+
module Couriers
|
12
|
+
module Spring
|
13
|
+
extend Courier
|
14
|
+
|
15
|
+
COURIER_ID = :spring
|
16
|
+
ENDPOINT_LIVE = 'https://mtapi.net/'.freeze
|
17
|
+
ENDPOINT_TEST = 'https://mtapi.net/?testMode=1'.freeze
|
18
|
+
|
19
|
+
Config = Struct.new(
|
20
|
+
:api_key,
|
21
|
+
:countries,
|
22
|
+
:default_product
|
23
|
+
)
|
24
|
+
|
25
|
+
module_function
|
26
|
+
|
27
|
+
def shipment_info(tracking_code:, **)
|
28
|
+
response = Shipments::Trace.new(
|
29
|
+
tracking_code: tracking_code
|
30
|
+
).execute
|
31
|
+
|
32
|
+
tracking_info_params = Shipments::Trace::FormatResponse.new(response: response).execute
|
33
|
+
Deliveries::TrackingInfo.new(tracking_info_params)
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_label(tracking_code:, **)
|
37
|
+
decoded_label, url = Labels::Generate.new(
|
38
|
+
tracking_code: tracking_code
|
39
|
+
).execute.values_at(:decoded_label, :url)
|
40
|
+
|
41
|
+
Deliveries::Label.new(
|
42
|
+
raw: decoded_label,
|
43
|
+
url: url
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
def get_labels(tracking_codes:, **)
|
48
|
+
labels = Deliveries::Labels.new
|
49
|
+
|
50
|
+
tracking_codes.each do |tracking_code|
|
51
|
+
labels << get_label(tracking_code: tracking_code)
|
52
|
+
end
|
53
|
+
|
54
|
+
labels
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_shipment(sender:, receiver:, parcels:, reference_code:, shipment_date: nil, **)
|
58
|
+
delivery = Shipments::Create.new(
|
59
|
+
sender: sender,
|
60
|
+
receiver: receiver,
|
61
|
+
parcels: parcels,
|
62
|
+
reference_code: reference_code
|
63
|
+
).execute
|
64
|
+
|
65
|
+
Deliveries::Shipment.new(delivery: delivery, shipment_date: shipment_date)
|
66
|
+
end
|
67
|
+
|
68
|
+
def create_pickup(sender:, receiver:, parcels:, reference_code:, pickup_date: nil, **)
|
69
|
+
delivery = Shipments::Create.new(
|
70
|
+
sender: sender,
|
71
|
+
receiver: receiver,
|
72
|
+
parcels: parcels,
|
73
|
+
reference_code: reference_code
|
74
|
+
).execute
|
75
|
+
|
76
|
+
Deliveries::Pickup.new(delivery: delivery, pickup_date: pickup_date)
|
77
|
+
end
|
78
|
+
|
79
|
+
def pickup_info(tracking_code:, **)
|
80
|
+
shipment_info(tracking_code: tracking_code)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,142 @@
|
|
1
|
+
module Deliveries
|
2
|
+
module Couriers
|
3
|
+
module Ups
|
4
|
+
module CollectionPoints
|
5
|
+
class Search
|
6
|
+
attr_accessor :country, :postcode, :point_id
|
7
|
+
|
8
|
+
def initialize(country:, postcode: nil, point_id: nil)
|
9
|
+
raise Error, 'Both postcode and point_id cannot be nil' if postcode.nil? && point_id.nil?
|
10
|
+
|
11
|
+
self.country = country.to_s.downcase
|
12
|
+
self.postcode = postcode
|
13
|
+
self.point_id = point_id
|
14
|
+
end
|
15
|
+
|
16
|
+
def execute
|
17
|
+
request_id = SecureRandom.uuid
|
18
|
+
|
19
|
+
access = Nokogiri::XML::Builder.new do |xml|
|
20
|
+
xml.AccessRequest('xml:lang': 'en-US') do
|
21
|
+
xml.AccessLicenseNumber Ups.config(:license_number)
|
22
|
+
xml.UserId Ups.config(:username)
|
23
|
+
xml.Password Ups.config(:password)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
locator = Nokogiri::XML::Builder.new do |xml|
|
28
|
+
xml.LocatorRequest do
|
29
|
+
xml.Request do
|
30
|
+
xml.RequestAction 'Locator'
|
31
|
+
xml.RequestOption '64'
|
32
|
+
xml.TransactionReference do
|
33
|
+
xml.CustomerContext request_id
|
34
|
+
xml.XpciVersion '1.0014'
|
35
|
+
end
|
36
|
+
end
|
37
|
+
xml.OriginAddress do
|
38
|
+
xml.AddressKeyFormat do
|
39
|
+
xml.SingleLineAddress postcode if postcode
|
40
|
+
xml.CountryCode country.upcase
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
xml.LocationSearchCriteria do
|
45
|
+
xml.AccessPointSearch do
|
46
|
+
xml.AccessPointStatus '01'
|
47
|
+
if point_id
|
48
|
+
xml.PublicAccessPointID point_id
|
49
|
+
else
|
50
|
+
xml.IncludeCriteria do
|
51
|
+
xml.SearchFilter do
|
52
|
+
xml.ShippingAvailabilityIndicator ''
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
xml.MaximumListSize '20'
|
59
|
+
end
|
60
|
+
|
61
|
+
xml.Translate do
|
62
|
+
xml.Locale locale
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
request_body = access.to_xml + locator.to_xml
|
68
|
+
|
69
|
+
response = HTTParty.post(
|
70
|
+
api_endpoint,
|
71
|
+
body: request_body,
|
72
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
73
|
+
debug_output: Deliveries.debug ? Deliveries.logger : nil
|
74
|
+
)
|
75
|
+
|
76
|
+
response_doc = Nokogiri.XML(response.body)
|
77
|
+
response_id = response_doc.at_xpath('//LocatorResponse/Response/TransactionReference/CustomerContext')&.content
|
78
|
+
raise Error, 'Request and response ID mismatch' if request_id != response_id
|
79
|
+
|
80
|
+
unless response_doc.at_xpath('//LocatorResponse/Response/ResponseStatusCode')&.content == '1'
|
81
|
+
error_code = response_doc.at_xpath('//LocatorResponse/Response/Error/ErrorCode')&.content
|
82
|
+
error_description = response_doc.at_xpath('//LocatorResponse/Response/Error/ErrorDescription')&.content
|
83
|
+
|
84
|
+
raise APIError.new(error_description, error_code)
|
85
|
+
end
|
86
|
+
|
87
|
+
response_doc.xpath('//LocatorResponse/SearchResults/DropLocation').map do |location_doc|
|
88
|
+
timetable = location_doc.xpath('OperatingHours/StandardHours/DayOfWeek').map do |day_doc|
|
89
|
+
wday = day_doc.at_xpath('Day')&.content.to_i % 7
|
90
|
+
open_hours = day_doc.xpath('OpenHours')&.map(&:content)&.map do |h|
|
91
|
+
h == '0' ? '00:00' : h.insert(-3, ':')
|
92
|
+
end || []
|
93
|
+
close_hours = day_doc.xpath('CloseHours')&.map(&:content)&.map do |h|
|
94
|
+
h == '0' ? '00:00' : h.insert(-3, ':')
|
95
|
+
end || []
|
96
|
+
hours = open_hours.zip(close_hours).map { |open, close| OpenStruct.new(open: open, close: close) }
|
97
|
+
|
98
|
+
[wday, hours]
|
99
|
+
end.to_h
|
100
|
+
|
101
|
+
{
|
102
|
+
courier_id: Ups::COURIER_ID,
|
103
|
+
point_id: location_doc.at_xpath('AccessPointInformation/PublicAccessPointID')&.content,
|
104
|
+
latitude: location_doc.at_xpath('Geocode/Latitude')&.content,
|
105
|
+
longitude: location_doc.at_xpath('Geocode/Longitude')&.content,
|
106
|
+
timetable: timetable,
|
107
|
+
url_photo: location_doc.at_xpath('AccessPointInformation/ImageURL')&.content,
|
108
|
+
name: location_doc.at_xpath('AddressKeyFormat/ConsigneeName')&.content,
|
109
|
+
email: location_doc.at_xpath('EMailAddress')&.content,
|
110
|
+
phone: location_doc.at_xpath('PhoneNumber')&.content,
|
111
|
+
country: location_doc.at_xpath('AddressKeyFormat/CountryCode')&.content&.downcase,
|
112
|
+
state: location_doc.at_xpath('AddressKeyFormat/PoliticalDivision1')&.content,
|
113
|
+
city: location_doc.at_xpath('AddressKeyFormat/PoliticalDivision2')&.content,
|
114
|
+
street: location_doc.at_xpath('AddressKeyFormat/AddressLine')&.content,
|
115
|
+
postcode: location_doc.at_xpath('AddressKeyFormat/PostcodePrimaryLow')&.content
|
116
|
+
}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def api_endpoint
|
123
|
+
'https://onlinetools.ups.com/ups.app/xml/Locator'
|
124
|
+
end
|
125
|
+
|
126
|
+
def locale
|
127
|
+
case country
|
128
|
+
when 'es' then 'es_ES'
|
129
|
+
when 'it' then 'it_IT'
|
130
|
+
when 'pt' then 'pt_PT'
|
131
|
+
when 'fr' then 'fr_FR'
|
132
|
+
when 'de' then 'de_DE'
|
133
|
+
when 'gb' then 'en_GB'
|
134
|
+
when 'pl' then 'pl_PL'
|
135
|
+
else 'en_US'
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module Deliveries
|
2
|
+
module Couriers
|
3
|
+
module Ups
|
4
|
+
module JsonRequest
|
5
|
+
private
|
6
|
+
|
7
|
+
def call(body, method: :post, url_params: {}, query_params: {})
|
8
|
+
plain_response = HTTParty.public_send(
|
9
|
+
method,
|
10
|
+
api_endpoint % url_params,
|
11
|
+
body: body&.to_json,
|
12
|
+
query: query_params,
|
13
|
+
headers: headers,
|
14
|
+
debug_output: Deliveries.debug ? Deliveries.logger : nil,
|
15
|
+
format: :plain
|
16
|
+
)
|
17
|
+
response = JSON.parse plain_response, symbolize_names: true
|
18
|
+
|
19
|
+
if response.dig(:response, :errors).present?
|
20
|
+
error_code = response.dig(:response, :errors, 0, :code)&.to_i
|
21
|
+
error_message = response.dig(:response, :errors, 0, :message) || 'Unknown error'
|
22
|
+
|
23
|
+
raise APIError.new(error_message, error_code)
|
24
|
+
end
|
25
|
+
|
26
|
+
response
|
27
|
+
end
|
28
|
+
|
29
|
+
def headers
|
30
|
+
{
|
31
|
+
'Content-Type': 'application/json',
|
32
|
+
AccessLicenseNumber: Ups.config(:license_number),
|
33
|
+
Username: Ups.config(:username),
|
34
|
+
Password: Ups.config(:password)
|
35
|
+
}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module Deliveries
|
2
|
+
module Couriers
|
3
|
+
module Ups
|
4
|
+
module Labels
|
5
|
+
class Generate
|
6
|
+
include JsonRequest
|
7
|
+
include LabelUtils
|
8
|
+
|
9
|
+
API_VERSION = 'V1903'.freeze
|
10
|
+
|
11
|
+
attr_accessor :tracking_code
|
12
|
+
|
13
|
+
def initialize(tracking_code:)
|
14
|
+
self.tracking_code = tracking_code
|
15
|
+
end
|
16
|
+
|
17
|
+
def execute
|
18
|
+
request = {
|
19
|
+
LabelRecoveryRequest: {
|
20
|
+
TrackingNumber: tracking_code,
|
21
|
+
ShipperNumber: account_number,
|
22
|
+
LabelSpecification: {
|
23
|
+
LabelImageFormat: {
|
24
|
+
Code: 'GIF'
|
25
|
+
}
|
26
|
+
},
|
27
|
+
LabelDelivery: {
|
28
|
+
LabelLinkIndicator: ''
|
29
|
+
}
|
30
|
+
}
|
31
|
+
}
|
32
|
+
|
33
|
+
response = call request
|
34
|
+
|
35
|
+
label_url = response.dig(:LabelRecoveryResponse, :LabelResults, :LabelImage, :URL)
|
36
|
+
label_encoded = response.dig(:LabelRecoveryResponse, :LabelResults, :LabelImage, :GraphicImage)
|
37
|
+
raise Error, 'Cannot obtain encoded label' if label_encoded.nil?
|
38
|
+
|
39
|
+
label_gif = Base64.decode64(label_encoded).force_encoding('binary')
|
40
|
+
label_pdf = image2pdf label_gif, height: 4
|
41
|
+
|
42
|
+
{
|
43
|
+
raw: label_pdf,
|
44
|
+
url: label_url
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def api_endpoint
|
51
|
+
if Ups.live?
|
52
|
+
"https://onlinetools.ups.com/ship/#{API_VERSION}/shipments/labels"
|
53
|
+
else
|
54
|
+
"https://wwwcie.ups.com/ship/#{API_VERSION}/shipments/labels"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def account_number
|
59
|
+
number_from_tracking_code = @tracking_code[/1Z([a-z0-9]{6})/i, 1]
|
60
|
+
if number_from_tracking_code&.casecmp? Ups.config(:point_account_number)
|
61
|
+
Ups.config(:point_account_number)
|
62
|
+
elsif number_from_tracking_code&.casecmp? Ups.config(:home_account_number)
|
63
|
+
Ups.config(:home_account_number)
|
64
|
+
else
|
65
|
+
raise Error, 'Invalid tracking code'
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|