deliveries 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +19 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +34 -0
  6. data/CHANGELOG.md +13 -0
  7. data/Gemfile +12 -0
  8. data/Gemfile.lock +127 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +207 -0
  11. data/Rakefile +12 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/deliveries.gemspec +36 -0
  15. data/lib/deliveries/address.rb +71 -0
  16. data/lib/deliveries/checkpoint.rb +12 -0
  17. data/lib/deliveries/collection_point.rb +45 -0
  18. data/lib/deliveries/courier.rb +69 -0
  19. data/lib/deliveries/couriers/correos_express/address.rb +31 -0
  20. data/lib/deliveries/couriers/correos_express/collection_points/search/format_response.rb +82 -0
  21. data/lib/deliveries/couriers/correos_express/collection_points/search.rb +56 -0
  22. data/lib/deliveries/couriers/correos_express/labels/generate.rb +68 -0
  23. data/lib/deliveries/couriers/correos_express/pickups/create/defaults.rb +60 -0
  24. data/lib/deliveries/couriers/correos_express/pickups/create/format_params.rb +95 -0
  25. data/lib/deliveries/couriers/correos_express/pickups/create.rb +66 -0
  26. data/lib/deliveries/couriers/correos_express/pickups/cutoff_time/format_params.rb +40 -0
  27. data/lib/deliveries/couriers/correos_express/pickups/cutoff_time.rb +61 -0
  28. data/lib/deliveries/couriers/correos_express/pickups/trace/correos.test.wsdl +163 -0
  29. data/lib/deliveries/couriers/correos_express/pickups/trace/correos.wsdl +163 -0
  30. data/lib/deliveries/couriers/correos_express/pickups/trace/format_response.rb +71 -0
  31. data/lib/deliveries/couriers/correos_express/pickups/trace.rb +49 -0
  32. data/lib/deliveries/couriers/correos_express/shipments/create/defaults.rb +80 -0
  33. data/lib/deliveries/couriers/correos_express/shipments/create/format_params.rb +99 -0
  34. data/lib/deliveries/couriers/correos_express/shipments/create.rb +101 -0
  35. data/lib/deliveries/couriers/correos_express/shipments/trace/format_response.rb +67 -0
  36. data/lib/deliveries/couriers/correos_express/shipments/trace.rb +65 -0
  37. data/lib/deliveries/couriers/correos_express.rb +162 -0
  38. data/lib/deliveries/couriers/dummy.rb +106 -0
  39. data/lib/deliveries/couriers/mondial_relay/address.rb +31 -0
  40. data/lib/deliveries/couriers/mondial_relay/collection_points/search/format_response.rb +103 -0
  41. data/lib/deliveries/couriers/mondial_relay/labels/generate.rb +40 -0
  42. data/lib/deliveries/couriers/mondial_relay/pickups/create/format_params.rb +57 -0
  43. data/lib/deliveries/couriers/mondial_relay/shipments/create/defaults.rb +96 -0
  44. data/lib/deliveries/couriers/mondial_relay/shipments/create/format_params.rb +68 -0
  45. data/lib/deliveries/couriers/mondial_relay/shipments/create.rb +36 -0
  46. data/lib/deliveries/couriers/mondial_relay/shipments/trace/format_response.rb +94 -0
  47. data/lib/deliveries/couriers/mondial_relay/shipments/trace.rb +47 -0
  48. data/lib/deliveries/couriers/mondial_relay/status_codes.rb +105 -0
  49. data/lib/deliveries/couriers/mondial_relay.rb +189 -0
  50. data/lib/deliveries/couriers/mondial_relay_dual/address.rb +31 -0
  51. data/lib/deliveries/couriers/mondial_relay_dual/pickups/create/format_params.rb +39 -0
  52. data/lib/deliveries/couriers/mondial_relay_dual/shipments/create/format_params.rb +101 -0
  53. data/lib/deliveries/couriers/mondial_relay_dual/shipments/create.rb +133 -0
  54. data/lib/deliveries/couriers/mondial_relay_dual.rb +105 -0
  55. data/lib/deliveries/couriers/spring/address.rb +23 -0
  56. data/lib/deliveries/couriers/spring/labels/generate.rb +33 -0
  57. data/lib/deliveries/couriers/spring/request.rb +43 -0
  58. data/lib/deliveries/couriers/spring/shipments/create/defaults.rb +73 -0
  59. data/lib/deliveries/couriers/spring/shipments/create/format_params.rb +77 -0
  60. data/lib/deliveries/couriers/spring/shipments/create.rb +45 -0
  61. data/lib/deliveries/couriers/spring/shipments/trace/format_response.rb +79 -0
  62. data/lib/deliveries/couriers/spring/shipments/trace.rb +29 -0
  63. data/lib/deliveries/couriers/spring.rb +84 -0
  64. data/lib/deliveries/couriers/ups/collection_points/search.rb +142 -0
  65. data/lib/deliveries/couriers/ups/json_request.rb +40 -0
  66. data/lib/deliveries/couriers/ups/labels/generate.rb +72 -0
  67. data/lib/deliveries/couriers/ups/shipments/create.rb +210 -0
  68. data/lib/deliveries/couriers/ups/shipments/trace.rb +79 -0
  69. data/lib/deliveries/couriers/ups.rb +122 -0
  70. data/lib/deliveries/couriers.rb +14 -0
  71. data/lib/deliveries/delivery.rb +20 -0
  72. data/lib/deliveries/errors.rb +22 -0
  73. data/lib/deliveries/label.rb +20 -0
  74. data/lib/deliveries/label_utils.rb +67 -0
  75. data/lib/deliveries/labels.rb +40 -0
  76. data/lib/deliveries/pickup.rb +15 -0
  77. data/lib/deliveries/shipment.rb +15 -0
  78. data/lib/deliveries/tracking_info.rb +29 -0
  79. data/lib/deliveries/version.rb +5 -0
  80. data/lib/deliveries.rb +63 -0
  81. 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