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.
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