deliveries 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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,210 @@
|
|
|
1
|
+
module Deliveries
|
|
2
|
+
module Couriers
|
|
3
|
+
module Ups
|
|
4
|
+
module Shipments
|
|
5
|
+
class Create
|
|
6
|
+
include JsonRequest
|
|
7
|
+
include LabelUtils
|
|
8
|
+
|
|
9
|
+
API_VERSION = 'v1807'.freeze
|
|
10
|
+
TYPES = %i[forward return].freeze
|
|
11
|
+
|
|
12
|
+
attr_accessor :shipper, :consignee, :parcels, :reference_code, :type, :collection_point, :language
|
|
13
|
+
|
|
14
|
+
def initialize(shipper:, consignee:, parcels:, reference_code:, type: :forward, collection_point: nil, language: nil)
|
|
15
|
+
raise ArgumentError, 'Invalid value for type' unless TYPES.include?(type)
|
|
16
|
+
|
|
17
|
+
self.shipper = shipper
|
|
18
|
+
self.consignee = consignee
|
|
19
|
+
self.parcels = parcels
|
|
20
|
+
self.reference_code = reference_code
|
|
21
|
+
self.collection_point = collection_point
|
|
22
|
+
self.type = type
|
|
23
|
+
self.language = language&.to_sym&.downcase || :en
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def execute
|
|
27
|
+
request = {
|
|
28
|
+
ShipmentRequest: {
|
|
29
|
+
Request: {
|
|
30
|
+
RequestOption: 'nonvalidate'
|
|
31
|
+
},
|
|
32
|
+
Shipment: {
|
|
33
|
+
Description: Ups.config('default_product.description', default: 'International Goods'),
|
|
34
|
+
|
|
35
|
+
Shipper: shipper_address(with_number: true),
|
|
36
|
+
|
|
37
|
+
PaymentInformation: {
|
|
38
|
+
ShipmentCharge: {
|
|
39
|
+
Type: '01',
|
|
40
|
+
BillShipper: {
|
|
41
|
+
AccountNumber: account_number
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
ReferenceNumber: {
|
|
47
|
+
Code: 'ON',
|
|
48
|
+
Value: reference_code
|
|
49
|
+
},
|
|
50
|
+
|
|
51
|
+
Service: {
|
|
52
|
+
Code: '11',
|
|
53
|
+
Description: 'Standard'
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
Package: package_data,
|
|
57
|
+
|
|
58
|
+
LabelSpecification: {
|
|
59
|
+
LabelImageFormat: {
|
|
60
|
+
Code: 'GIF'
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if type == :return
|
|
68
|
+
request[:ShipmentRequest][:Shipment][:ReturnService] = { Code: '9' }
|
|
69
|
+
request[:ShipmentRequest][:Shipment][:ShipFrom] = consignee_address
|
|
70
|
+
request[:ShipmentRequest][:Shipment][:ShipTo] = shipper_address
|
|
71
|
+
else # forward
|
|
72
|
+
request[:ShipmentRequest][:Shipment][:ShipFrom] = shipper_address
|
|
73
|
+
request[:ShipmentRequest][:Shipment][:ShipTo] = consignee_address
|
|
74
|
+
|
|
75
|
+
if collection_point?
|
|
76
|
+
request[:ShipmentRequest][:Shipment][:ShipmentIndicationType] = {
|
|
77
|
+
Code: '01',
|
|
78
|
+
Description: 'DirectToRetail'
|
|
79
|
+
}
|
|
80
|
+
request[:ShipmentRequest][:Shipment][:AlternateDeliveryAddress] = collection_point_address
|
|
81
|
+
request[:ShipmentRequest][:Shipment][:ShipmentServiceOptions] = {
|
|
82
|
+
Notification: {
|
|
83
|
+
NotificationCode: '012',
|
|
84
|
+
EMail: {
|
|
85
|
+
EMailAddress: consignee.email
|
|
86
|
+
},
|
|
87
|
+
Locale: {
|
|
88
|
+
Language: locale_language,
|
|
89
|
+
Dialect: locale_dialect
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
response = call request
|
|
97
|
+
|
|
98
|
+
tracking_code = response.dig(:ShipmentResponse, :ShipmentResults, :ShipmentIdentificationNumber)
|
|
99
|
+
labels = [response.dig(:ShipmentResponse, :ShipmentResults, :PackageResults)].flatten.map do |p|
|
|
100
|
+
encoded_gif = p.dig(:ShippingLabel, :GraphicImage)
|
|
101
|
+
image2pdf Base64.decode64(encoded_gif).force_encoding('binary'), height: 4
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
tracking_code: tracking_code,
|
|
106
|
+
label: labels.one? ? labels.first : merge_pdfs(labels)
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
private
|
|
111
|
+
|
|
112
|
+
def api_endpoint
|
|
113
|
+
if Ups.live?
|
|
114
|
+
"https://onlinetools.ups.com/ship/#{API_VERSION}/shipments"
|
|
115
|
+
else
|
|
116
|
+
"https://wwwcie.ups.com/ship/#{API_VERSION}/shipments"
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def collection_point?
|
|
121
|
+
!@collection_point.nil?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def account_number
|
|
125
|
+
if collection_point?
|
|
126
|
+
Ups.config(:point_account_number)
|
|
127
|
+
else
|
|
128
|
+
Ups.config(:home_account_number)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def locale_language
|
|
133
|
+
case language
|
|
134
|
+
when :es then 'SPA'
|
|
135
|
+
when :fr then 'FRA'
|
|
136
|
+
when :pt then 'POR'
|
|
137
|
+
when :it then 'ITA'
|
|
138
|
+
when :de then 'DEU'
|
|
139
|
+
when :pl then 'POL'
|
|
140
|
+
else 'ENG'
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def locale_dialect
|
|
145
|
+
if language == :en
|
|
146
|
+
'GB'
|
|
147
|
+
else
|
|
148
|
+
'97'
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def package_data
|
|
153
|
+
parcels.times.map do
|
|
154
|
+
{
|
|
155
|
+
Description: Ups.config('default_product.description', default: 'International Goods'),
|
|
156
|
+
Packaging: {
|
|
157
|
+
Code: '02',
|
|
158
|
+
Description: 'Customer Supplied'
|
|
159
|
+
},
|
|
160
|
+
PackageWeight: {
|
|
161
|
+
UnitOfMeasurement: {
|
|
162
|
+
Code: 'KGS'
|
|
163
|
+
},
|
|
164
|
+
Weight: Ups.config('default_product.weight', default: '1')
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def shipper_address(with_number: false)
|
|
171
|
+
address = format_address shipper
|
|
172
|
+
address[:ShipperNumber] = account_number if with_number
|
|
173
|
+
|
|
174
|
+
address
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def consignee_address
|
|
178
|
+
format_address consignee
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def collection_point_address
|
|
182
|
+
address = format_address collection_point
|
|
183
|
+
address[:UPSAccessPointID] = collection_point.point_id
|
|
184
|
+
address.delete :Phone
|
|
185
|
+
address.delete :EMailAddress
|
|
186
|
+
|
|
187
|
+
address
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def format_address(address)
|
|
191
|
+
{
|
|
192
|
+
Name: I18n.transliterate(address.name)[0, 35],
|
|
193
|
+
AttentionName: I18n.transliterate(address.name)[0, 35],
|
|
194
|
+
Phone: {
|
|
195
|
+
Number: address.phone
|
|
196
|
+
},
|
|
197
|
+
EMailAddress: address.email,
|
|
198
|
+
Address: {
|
|
199
|
+
AddressLine: I18n.transliterate(address.street).scan(/.{1,35}/)[0, 3],
|
|
200
|
+
City: I18n.transliterate(address.city)[0, 30],
|
|
201
|
+
PostalCode: address.postcode.to_s[0, 9],
|
|
202
|
+
CountryCode: address.country.to_s.downcase[0, 2]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
module Deliveries
|
|
2
|
+
module Couriers
|
|
3
|
+
module Ups
|
|
4
|
+
module Shipments
|
|
5
|
+
class Trace
|
|
6
|
+
include JsonRequest
|
|
7
|
+
|
|
8
|
+
attr_accessor :tracking_code, :language
|
|
9
|
+
|
|
10
|
+
def initialize(tracking_code:, language: nil)
|
|
11
|
+
self.tracking_code = tracking_code
|
|
12
|
+
self.language = language
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def execute
|
|
16
|
+
response = call nil, method: :get, url_params: { inquiry_number: tracking_code },
|
|
17
|
+
query_params: { locale: locale }
|
|
18
|
+
|
|
19
|
+
package = response.dig(:trackResponse, :shipment, 0, :package, 0, :activity)
|
|
20
|
+
raise Error, 'Cannot obtain package activity data' if package.blank?
|
|
21
|
+
|
|
22
|
+
activities = package.map do |activity|
|
|
23
|
+
{
|
|
24
|
+
status: status(activity.dig(:status, :type), activity.dig(:status, :code)),
|
|
25
|
+
location: activity.dig(:location, :address, :city),
|
|
26
|
+
tracked_at: DateTime.parse("#{activity[:date]}T#{activity[:time]}"),
|
|
27
|
+
description: activity.dig(:status, :description)
|
|
28
|
+
}
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
activities.sort_by { |activity| activity[:tracked_at] }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def api_endpoint
|
|
37
|
+
if Ups.live?
|
|
38
|
+
'https://onlinetools.ups.com/track/v1/details/%<inquiry_number>s'
|
|
39
|
+
else
|
|
40
|
+
'https://wwwcie.ups.com/track/v1/details/%<inquiry_number>s'
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def locale
|
|
45
|
+
case language&.to_sym&.downcase
|
|
46
|
+
when :es then 'es_ES'
|
|
47
|
+
when :fr then 'fr_FR'
|
|
48
|
+
when :pt then 'pt_PT'
|
|
49
|
+
when :it then 'it_IT'
|
|
50
|
+
when :de then 'de_DE'
|
|
51
|
+
when :en then 'en_GB'
|
|
52
|
+
when :pl then 'pl_PL'
|
|
53
|
+
else 'en_US'
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def status(type, code)
|
|
58
|
+
case type&.to_s&.upcase
|
|
59
|
+
when 'M'
|
|
60
|
+
:registered
|
|
61
|
+
when 'I', 'P', 'O'
|
|
62
|
+
:in_transit
|
|
63
|
+
when 'D'
|
|
64
|
+
if %w[2Q ZP].include?(code&.to_s&.upcase)
|
|
65
|
+
:in_collection_point
|
|
66
|
+
else
|
|
67
|
+
:delivered
|
|
68
|
+
end
|
|
69
|
+
when 'MV'
|
|
70
|
+
:canceled
|
|
71
|
+
else
|
|
72
|
+
:unknown_status
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
require_relative 'ups/json_request'
|
|
2
|
+
require_relative 'ups/collection_points/search'
|
|
3
|
+
require_relative 'ups/shipments/create'
|
|
4
|
+
require_relative 'ups/shipments/trace'
|
|
5
|
+
require_relative 'ups/labels/generate'
|
|
6
|
+
|
|
7
|
+
module Deliveries
|
|
8
|
+
module Couriers
|
|
9
|
+
module Ups
|
|
10
|
+
extend Courier
|
|
11
|
+
|
|
12
|
+
COURIER_ID = :ups
|
|
13
|
+
|
|
14
|
+
Config = Struct.new(
|
|
15
|
+
:license_number,
|
|
16
|
+
:username,
|
|
17
|
+
:password,
|
|
18
|
+
:point_account_number,
|
|
19
|
+
:home_account_number,
|
|
20
|
+
:default_product
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
module_function
|
|
24
|
+
|
|
25
|
+
def get_collection_point(global_point_id:)
|
|
26
|
+
global_point = CollectionPoint.parse_global_point_id(global_point_id: global_point_id)
|
|
27
|
+
points = CollectionPoints::Search.new(
|
|
28
|
+
country: global_point.country,
|
|
29
|
+
point_id: global_point.point_id
|
|
30
|
+
).execute
|
|
31
|
+
|
|
32
|
+
raise Error, 'No collection point found by that point ID' if points.empty?
|
|
33
|
+
|
|
34
|
+
CollectionPoint.new(**points.first)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def get_collection_points(country:, postcode:)
|
|
38
|
+
points = CollectionPoints::Search.new(
|
|
39
|
+
country: country,
|
|
40
|
+
postcode: postcode
|
|
41
|
+
).execute
|
|
42
|
+
|
|
43
|
+
points.map do |arguments|
|
|
44
|
+
CollectionPoint.new(**arguments)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def create_shipment(sender:, receiver:, parcels:, reference_code:, collection_point:, shipment_date: nil, language: nil, **)
|
|
49
|
+
tracking_code, label = Shipments::Create.new(
|
|
50
|
+
shipper: sender,
|
|
51
|
+
consignee: receiver,
|
|
52
|
+
parcels: parcels,
|
|
53
|
+
reference_code: reference_code,
|
|
54
|
+
type: :forward,
|
|
55
|
+
collection_point: collection_point,
|
|
56
|
+
language: language
|
|
57
|
+
).execute.values_at(:tracking_code, :label)
|
|
58
|
+
|
|
59
|
+
Deliveries::Shipment.new(
|
|
60
|
+
courier_id: COURIER_ID,
|
|
61
|
+
sender: sender,
|
|
62
|
+
receiver: receiver,
|
|
63
|
+
parcels: parcels,
|
|
64
|
+
reference_code: reference_code,
|
|
65
|
+
tracking_code: tracking_code,
|
|
66
|
+
shipment_date: shipment_date,
|
|
67
|
+
label: Label.new(raw: label)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def create_pickup(sender:, receiver:, parcels:, reference_code:, pickup_date: nil, language: nil, **)
|
|
72
|
+
tracking_code, label = Shipments::Create.new(
|
|
73
|
+
shipper: receiver,
|
|
74
|
+
consignee: sender,
|
|
75
|
+
parcels: parcels,
|
|
76
|
+
reference_code: reference_code,
|
|
77
|
+
type: :return,
|
|
78
|
+
language: language
|
|
79
|
+
).execute.values_at(:tracking_code, :label)
|
|
80
|
+
|
|
81
|
+
Deliveries::Pickup.new(
|
|
82
|
+
courier_id: COURIER_ID,
|
|
83
|
+
sender: sender,
|
|
84
|
+
receiver: receiver,
|
|
85
|
+
parcels: parcels,
|
|
86
|
+
reference_code: reference_code,
|
|
87
|
+
tracking_code: tracking_code,
|
|
88
|
+
pickup_date: pickup_date,
|
|
89
|
+
label: Label.new(raw: label)
|
|
90
|
+
)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def get_label(tracking_code:, **)
|
|
94
|
+
arguments = Labels::Generate.new(tracking_code: tracking_code).execute
|
|
95
|
+
Label.new(**arguments)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def get_labels(tracking_codes:, **)
|
|
99
|
+
labels = Deliveries::Labels.new
|
|
100
|
+
|
|
101
|
+
tracking_codes.each do |tracking_code|
|
|
102
|
+
labels << get_label(tracking_code: tracking_code)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
labels
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def shipment_info(tracking_code:, language: nil)
|
|
109
|
+
checkpoints = Shipments::Trace.new(tracking_code: tracking_code, language: language).execute.map do |arguments|
|
|
110
|
+
Checkpoint.new(**arguments)
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
TrackingInfo.new(courier_id: COURIER_ID, tracking_code: tracking_code, checkpoints: checkpoints,
|
|
114
|
+
status: checkpoints.last&.status)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def pickup_info(tracking_code:, language: nil)
|
|
118
|
+
shipment_info(tracking_code: tracking_code, language: language)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Deliveries
|
|
2
|
+
module Couriers
|
|
3
|
+
Dir["#{File.dirname(__FILE__)}/couriers/*.rb"].each do |f|
|
|
4
|
+
# Get camelized class name
|
|
5
|
+
filename = File.basename(f, '.rb')
|
|
6
|
+
|
|
7
|
+
# Camelize the string to get the class name
|
|
8
|
+
courier_class = filename.split('_').map(&:capitalize).join.to_sym
|
|
9
|
+
|
|
10
|
+
# Register for autoloading
|
|
11
|
+
autoload courier_class, f
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Deliveries
|
|
2
|
+
class Delivery
|
|
3
|
+
ATTRIBUTES = %i[courier_id sender receiver parcels reference_code tracking_code label].freeze
|
|
4
|
+
attr_accessor(*ATTRIBUTES)
|
|
5
|
+
|
|
6
|
+
def initialize(**attributes)
|
|
7
|
+
self.courier_id = attributes[:courier_id]
|
|
8
|
+
self.sender = attributes[:sender]
|
|
9
|
+
self.receiver = attributes[:receiver]
|
|
10
|
+
self.parcels = attributes[:parcels]
|
|
11
|
+
self.reference_code = attributes[:reference_code]
|
|
12
|
+
self.tracking_code = attributes[:tracking_code]
|
|
13
|
+
self.label = attributes[:label]
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def attributes
|
|
17
|
+
ATTRIBUTES.map { |attr| { attr => send(attr) } }.inject(&:merge)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Deliveries
|
|
2
|
+
# Generic Deliveries exception class.
|
|
3
|
+
class Error < StandardError; end
|
|
4
|
+
|
|
5
|
+
# For errors returned by the client library, like HTTP errors
|
|
6
|
+
class ClientError < Error; end
|
|
7
|
+
|
|
8
|
+
# For errors returned by the API
|
|
9
|
+
class APIError < Error
|
|
10
|
+
attr_reader :code
|
|
11
|
+
|
|
12
|
+
def initialize(message = nil, code = nil)
|
|
13
|
+
@code = code
|
|
14
|
+
super(message)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Custom API errors.
|
|
19
|
+
class InvalidDateError < APIError; end
|
|
20
|
+
|
|
21
|
+
class InvalidTimeIntervalError < APIError; end
|
|
22
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Deliveries
|
|
2
|
+
class Label
|
|
3
|
+
attr_reader :url
|
|
4
|
+
|
|
5
|
+
def initialize(raw: nil, url: nil)
|
|
6
|
+
raise ArgumentError, 'Both raw and url cannot be nil' if raw.nil? && url.nil?
|
|
7
|
+
|
|
8
|
+
@raw = raw
|
|
9
|
+
@url = url
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def raw
|
|
13
|
+
if @raw
|
|
14
|
+
@raw
|
|
15
|
+
elsif @url
|
|
16
|
+
@raw = URI.parse(@url).read.force_encoding('binary')
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'mini_magick'
|
|
2
|
+
require 'hexapdf'
|
|
3
|
+
|
|
4
|
+
module Deliveries
|
|
5
|
+
module LabelUtils
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def image2pdf(bindata, width: nil, height: nil)
|
|
9
|
+
Tempfile.create do |f|
|
|
10
|
+
f.binmode
|
|
11
|
+
|
|
12
|
+
f << bindata
|
|
13
|
+
|
|
14
|
+
image = MiniMagick::Image.open(f.path)
|
|
15
|
+
image.format 'png' unless image.data['format']&.casecmp?('png')
|
|
16
|
+
|
|
17
|
+
doc = HexaPDF::Document.new
|
|
18
|
+
doc_image = doc.images.add(image.path)
|
|
19
|
+
iw = doc_image.info.width.to_f
|
|
20
|
+
ih = doc_image.info.height.to_f
|
|
21
|
+
media_box =
|
|
22
|
+
if width.nil? && height.nil?
|
|
23
|
+
[0, 0, iw, ih]
|
|
24
|
+
elsif width.nil?
|
|
25
|
+
width = (height * iw) / ih
|
|
26
|
+
[0, 0, width * 72, height * 72]
|
|
27
|
+
elsif height.nil?
|
|
28
|
+
height = (width * ih) / iw
|
|
29
|
+
[0, 0, width * 72, height * 72]
|
|
30
|
+
else
|
|
31
|
+
[0, 0, iw, ih]
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if (ih > iw) != (media_box[3] > media_box[2]) && (iw > media_box[2] || ih > media_box[3])
|
|
35
|
+
media_box[2], media_box[3] = media_box[3], media_box[2]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
page = doc.pages.add(media_box)
|
|
39
|
+
pw = page.box(:media).width.to_f
|
|
40
|
+
ph = page.box(:media).height.to_f
|
|
41
|
+
ratio = [pw / iw, ph / ih].min
|
|
42
|
+
iw *= ratio
|
|
43
|
+
ih *= ratio
|
|
44
|
+
x = (pw - iw) / 2
|
|
45
|
+
y = (ph - ih) / 2
|
|
46
|
+
page.canvas.image(doc_image, at: [x, y], width: iw, height: ih)
|
|
47
|
+
|
|
48
|
+
output = StringIO.new
|
|
49
|
+
doc.write(output)
|
|
50
|
+
output.string.force_encoding('binary')
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def merge_pdfs(*pdfs)
|
|
55
|
+
doc = HexaPDF::Document.new
|
|
56
|
+
pdfs.flatten.each do |pdf|
|
|
57
|
+
HexaPDF::Document.new(io: StringIO.new(pdf)).pages.each do |page|
|
|
58
|
+
doc.pages << doc.import(page)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
output = StringIO.new
|
|
63
|
+
doc.write(output)
|
|
64
|
+
output.string.force_encoding('binary')
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
module Deliveries
|
|
2
|
+
class Labels
|
|
3
|
+
include LabelUtils
|
|
4
|
+
|
|
5
|
+
attr_reader :url
|
|
6
|
+
|
|
7
|
+
def initialize(raw: nil, url: nil)
|
|
8
|
+
@raw = raw
|
|
9
|
+
@url = url
|
|
10
|
+
@labels = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def raw
|
|
14
|
+
if @raw
|
|
15
|
+
@raw
|
|
16
|
+
elsif @url
|
|
17
|
+
@raw = URI.parse(@url).read.force_encoding('binary')
|
|
18
|
+
elsif !@labels.empty?
|
|
19
|
+
merge_pdfs @labels.map(&:raw)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def <<(label)
|
|
24
|
+
raise 'Cannot add labels when raw or url are already set' unless @raw.nil? && @url.nil?
|
|
25
|
+
|
|
26
|
+
case label
|
|
27
|
+
when Label
|
|
28
|
+
@labels << label
|
|
29
|
+
when %r{\Ahttps?://}
|
|
30
|
+
@labels << Label.new(url: label)
|
|
31
|
+
when String
|
|
32
|
+
@labels << Label.new(raw: label)
|
|
33
|
+
else
|
|
34
|
+
raise "Cannot cast #{label.class.name} to Label"
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
self
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Deliveries
|
|
2
|
+
class Pickup < Delivery
|
|
3
|
+
attr_accessor :pickup_date
|
|
4
|
+
|
|
5
|
+
def initialize(delivery: nil, **attributes)
|
|
6
|
+
if delivery.is_a? Deliveries::Delivery
|
|
7
|
+
super(**delivery.attributes)
|
|
8
|
+
else
|
|
9
|
+
super(**attributes)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
self.pickup_date = attributes[:pickup_date]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module Deliveries
|
|
2
|
+
class Shipment < Delivery
|
|
3
|
+
attr_accessor :shipment_date
|
|
4
|
+
|
|
5
|
+
def initialize(delivery: nil, **attributes)
|
|
6
|
+
if delivery.is_a? Deliveries::Delivery
|
|
7
|
+
super(**delivery.attributes)
|
|
8
|
+
else
|
|
9
|
+
super(**attributes)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
self.shipment_date = attributes[:shipment_date]
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Deliveries
|
|
2
|
+
class TrackingInfo
|
|
3
|
+
attr_accessor :courier_id, :tracking_code, :status, :checkpoints, :url
|
|
4
|
+
|
|
5
|
+
def initialize(courier_id:, tracking_code:, status: nil, checkpoints: nil, url: nil)
|
|
6
|
+
self.courier_id = courier_id
|
|
7
|
+
self.tracking_code = tracking_code
|
|
8
|
+
self.status = status
|
|
9
|
+
self.checkpoints = checkpoints
|
|
10
|
+
self.url = url
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def registered?
|
|
14
|
+
status == :registered
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def in_transit?
|
|
18
|
+
status == :in_transit
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def in_collection_point?
|
|
22
|
+
status == :in_collection_point
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def delivered?
|
|
26
|
+
status == :delivered
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|