an_post_return 0.2.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/.cursor/mcp.json +21 -0
- data/.cursor/rules/cursor_rules.mdc +53 -0
- data/.cursor/rules/dev_workflow.mdc +215 -0
- data/.cursor/rules/self_improve.mdc +73 -0
- data/.cursor/rules/taskmaster.mdc +353 -0
- data/.env.example +14 -0
- data/.rspec +3 -0
- data/.standard.yml +3 -0
- data/.windsurfrules +474 -0
- data/CHANGELOG.md +5 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/LICENSE.txt +21 -0
- data/README-task-master.md +645 -0
- data/README.md +166 -0
- data/Rakefile +10 -0
- data/lib/an_post_return/client.rb +82 -0
- data/lib/an_post_return/configuration.rb +47 -0
- data/lib/an_post_return/errors.rb +21 -0
- data/lib/an_post_return/objects/base.rb +70 -0
- data/lib/an_post_return/objects/return_label.rb +11 -0
- data/lib/an_post_return/resources/return_label_resource.rb +123 -0
- data/lib/an_post_return/sftp/client.rb +132 -0
- data/lib/an_post_return/sftp/errors.rb +12 -0
- data/lib/an_post_return/sftp/tracking_parser.rb +116 -0
- data/lib/an_post_return/tracker.rb +97 -0
- data/lib/an_post_return/version.rb +5 -0
- data/lib/an_post_return.rb +45 -0
- data/scripts/example_prd.txt +47 -0
- data/scripts/prd.txt +147 -0
- data/scripts/sftp_test.rb +185 -0
- data/scripts/task-complexity-report.json +131 -0
- data/sig/anpost_api.rbs +4 -0
- metadata +302 -0
@@ -0,0 +1,116 @@
|
|
1
|
+
require "csv"
|
2
|
+
require "time"
|
3
|
+
require_relative "../errors"
|
4
|
+
|
5
|
+
module AnPostReturn
|
6
|
+
module SFTP
|
7
|
+
class TrackingParser
|
8
|
+
REQUIRED_FIELDS = %w[tracking_number status timestamp].freeze
|
9
|
+
TIMESTAMP_FORMAT = "%Y-%m-%d %H:%M:%S".freeze
|
10
|
+
|
11
|
+
# Parse tracking data from a CSV file
|
12
|
+
#
|
13
|
+
# @param file [File, Tempfile] CSV file to parse
|
14
|
+
# @return [Array<Hash>] Array of tracking data entries
|
15
|
+
def self.parse(file)
|
16
|
+
new.parse(file)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Parse tracking data from a text file
|
20
|
+
#
|
21
|
+
# @param file_path [String] Path to the text file to parse
|
22
|
+
# @return [Hash] Hash containing header, data records, and footer information
|
23
|
+
# @raise [AnPostReturn::ParserError] if file is not found or empty
|
24
|
+
def parse(file_path)
|
25
|
+
validate_file!(file_path)
|
26
|
+
|
27
|
+
result = { header: nil, data: [], footer: nil }
|
28
|
+
|
29
|
+
File.foreach(file_path) do |line|
|
30
|
+
line = line.strip
|
31
|
+
next if line.empty?
|
32
|
+
|
33
|
+
# Determine delimiter and parse fields
|
34
|
+
delimiter = line.include?("+") ? "+" : ","
|
35
|
+
fields = CSV.parse_line(line, col_sep: delimiter, quote_char: '"')
|
36
|
+
record_type = fields[0]
|
37
|
+
|
38
|
+
case record_type
|
39
|
+
when "00"
|
40
|
+
result[:header] = parse_header(fields)
|
41
|
+
when "01"
|
42
|
+
result[:data] << parse_data_record(fields)
|
43
|
+
when "99"
|
44
|
+
result[:footer] = parse_footer(fields)
|
45
|
+
else
|
46
|
+
next # Skip unknown record types
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
result
|
51
|
+
rescue CSV::MalformedCSVError => e
|
52
|
+
raise ParserError, "Invalid file format: #{e.message}"
|
53
|
+
rescue => e
|
54
|
+
raise ParserError, "Error parsing tracking data: #{e.message}"
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
def validate_file!(file_path)
|
60
|
+
raise ParserError, "File not found: #{file_path}" unless File.exist?(file_path)
|
61
|
+
raise ParserError, "Empty file: #{file_path}" if File.zero?(file_path)
|
62
|
+
end
|
63
|
+
|
64
|
+
def parse_header(fields)
|
65
|
+
{
|
66
|
+
record_type: fields[0],
|
67
|
+
file_id: fields[1],
|
68
|
+
timestamp: format_timestamp(fields[2]),
|
69
|
+
record_count: fields[3].to_i,
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_data_record(fields)
|
74
|
+
{
|
75
|
+
record_type: fields[0],
|
76
|
+
service: fields[1],
|
77
|
+
tracking_number: fields[2],
|
78
|
+
country: fields[3],
|
79
|
+
combined_id: fields[4],
|
80
|
+
status: fields[5],
|
81
|
+
timestamp: format_timestamp(fields[6]),
|
82
|
+
location: fields[7],
|
83
|
+
notes: fields[8],
|
84
|
+
additional_info: fields[9],
|
85
|
+
outcome: fields[10],
|
86
|
+
recipient: fields[11],
|
87
|
+
extra1: fields[12],
|
88
|
+
extra2: fields[13],
|
89
|
+
}.compact
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_footer(fields)
|
93
|
+
{ record_type: fields[0], record_count: fields[1].to_i }
|
94
|
+
end
|
95
|
+
|
96
|
+
def format_timestamp(timestamp)
|
97
|
+
return nil if timestamp.nil? || timestamp.empty?
|
98
|
+
|
99
|
+
if timestamp.length >= 14
|
100
|
+
year = timestamp[0..3]
|
101
|
+
month = timestamp[4..5]
|
102
|
+
day = timestamp[6..7]
|
103
|
+
hour = timestamp[8..9]
|
104
|
+
minute = timestamp[10..11]
|
105
|
+
second = timestamp[12..13]
|
106
|
+
|
107
|
+
Time.new(year.to_i, month.to_i, day.to_i, hour.to_i, minute.to_i, second.to_i)
|
108
|
+
else
|
109
|
+
Time.parse(timestamp)
|
110
|
+
end
|
111
|
+
rescue ArgumentError => e
|
112
|
+
nil # Return nil for invalid timestamps instead of raising error
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
require_relative "sftp/client"
|
2
|
+
require_relative "sftp/tracking_parser"
|
3
|
+
require_relative "configuration"
|
4
|
+
|
5
|
+
module AnPostReturn
|
6
|
+
class Tracker
|
7
|
+
# Initialize a new Tracking resource
|
8
|
+
def initialize
|
9
|
+
@config = AnPostReturn.configuration
|
10
|
+
raise ArgumentError, "SFTP configuration is not set" unless @config.sftp_configured?
|
11
|
+
end
|
12
|
+
|
13
|
+
# Track files with a specific account number
|
14
|
+
#
|
15
|
+
# @param account_number [String] The account number to track
|
16
|
+
# @param last [Integer] The number of files to track
|
17
|
+
# @yieldparam data [Hash] Parsed tracking data containing :header, :data, and :footer
|
18
|
+
# @return [void]
|
19
|
+
# @raise [AnPostReturn::SFTP::ConnectionError] if SFTP connection fails
|
20
|
+
# @raise [AnPostReturn::SFTP::FileError] if file operations fail
|
21
|
+
def track_with_account_number(account_number, last: 0, &block)
|
22
|
+
# pad the account number with leading zeros to 8 digits
|
23
|
+
account_number = account_number.to_s.rjust(8, "0")
|
24
|
+
with_sftp_client do |sftp_client|
|
25
|
+
file =
|
26
|
+
if last.zero?
|
27
|
+
sftp_client.list_files(@config.sftp_config[:remote_path], "cdt#{account_number}*").first
|
28
|
+
else
|
29
|
+
sftp_client.list_files(@config.sftp_config[:remote_path], "cdt#{account_number}*")[-(last + 1)]
|
30
|
+
end
|
31
|
+
track_from(file.name, sftp_client, &block) if file
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Get tracking data from a file, incrementing file number if needed
|
36
|
+
#
|
37
|
+
# @param last_filename [String, nil] Base last filename processed (e.g. "cdt0370132115864.txt")
|
38
|
+
# @param existing_sftp_client [SFTP::Client, nil] Existing SFTP client to use
|
39
|
+
# @yieldparam data [Hash] Parsed tracking data containing :header, :data, and :footer
|
40
|
+
# @return [void]
|
41
|
+
# @raise [AnPostReturn::SFTP::ConnectionError] if SFTP connection fails
|
42
|
+
# @raise [AnPostReturn::SFTP::FileError] if file operations fail
|
43
|
+
# @raise [AnPostReturn::ParserError] if parsing fails
|
44
|
+
def track_from(last_filename, existing_sftp_client = nil, &block)
|
45
|
+
return unless block_given?
|
46
|
+
|
47
|
+
with_sftp_client(existing_sftp_client) do |client|
|
48
|
+
# example file name: CDT99999999SSSSS.txt
|
49
|
+
# Where:
|
50
|
+
# • CDT is to prefix each file (Customer Data Tracking).
|
51
|
+
# • 99999999 is the An Post Customer Account Number.
|
52
|
+
# • SSSSS is a sequence number starting at 1 and incrementing by 1 for every file sent, with leading zeros.
|
53
|
+
# • .txt is the standard file extension.
|
54
|
+
#
|
55
|
+
# extract the customer account number, sequence number and extension
|
56
|
+
customer_account_number = last_filename.match(/^cdt(\d+)([0-9]{5})\.txt$/)[1]
|
57
|
+
sequence_number = last_filename.match(/^cdt(\d+)([0-9]{5})\.txt$/)[2]
|
58
|
+
extension = ".txt"
|
59
|
+
|
60
|
+
while true
|
61
|
+
# increment the sequence number
|
62
|
+
sequence_number = sequence_number.to_i + 1
|
63
|
+
# format the new filename
|
64
|
+
next_filename = "cdt#{customer_account_number}#{sequence_number.to_s.rjust(5, "0")}#{extension}"
|
65
|
+
|
66
|
+
client.read_file(next_filename) do |tracking_file|
|
67
|
+
data = SFTP::TrackingParser.parse(tracking_file)
|
68
|
+
yield next_filename, data
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
rescue SFTP::FileNotFoundError
|
73
|
+
# If file not found, we're done
|
74
|
+
return
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def with_sftp_client(existing_sftp_client = nil)
|
80
|
+
if existing_sftp_client
|
81
|
+
client = existing_sftp_client
|
82
|
+
else
|
83
|
+
client =
|
84
|
+
SFTP::Client.new(
|
85
|
+
@config.sftp_config[:host],
|
86
|
+
@config.sftp_config[:username],
|
87
|
+
@config.sftp_config[:password],
|
88
|
+
@config.proxy_config,
|
89
|
+
)
|
90
|
+
client.connect
|
91
|
+
end
|
92
|
+
yield client
|
93
|
+
ensure
|
94
|
+
client&.disconnect unless existing_sftp_client
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "faraday"
|
4
|
+
require "json"
|
5
|
+
require "net/sftp"
|
6
|
+
|
7
|
+
require_relative "an_post_return/version"
|
8
|
+
require_relative "an_post_return/errors"
|
9
|
+
|
10
|
+
module AnPostReturn
|
11
|
+
class << self
|
12
|
+
attr_writer :configuration
|
13
|
+
|
14
|
+
def configuration
|
15
|
+
@configuration ||= Configuration.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def configure
|
19
|
+
yield(configuration)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
# Autoload core classes
|
24
|
+
autoload :Configuration, "an_post_return/configuration"
|
25
|
+
autoload :Client, "an_post_return/client"
|
26
|
+
|
27
|
+
# Autoload object classes
|
28
|
+
autoload :Base, "an_post_return/objects/base"
|
29
|
+
autoload :ReturnLabel, "an_post_return/objects/return_label"
|
30
|
+
|
31
|
+
# Autoload tracker
|
32
|
+
autoload :Tracker, "an_post_return/tracker"
|
33
|
+
|
34
|
+
# Autoload resource classes
|
35
|
+
module Resources
|
36
|
+
autoload :Base, "an_post_return/resources/base"
|
37
|
+
autoload :ReturnLabelResource, "an_post_return/resources/return_label_resource"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Autoload SFTP related classes
|
41
|
+
module SFTP
|
42
|
+
autoload :Client, "an_post_return/sftp/client"
|
43
|
+
autoload :Report, "an_post_return/sftp/report"
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
<context>
|
2
|
+
# Overview
|
3
|
+
[Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.]
|
4
|
+
|
5
|
+
# Core Features
|
6
|
+
[List and describe the main features of your product. For each feature, include:
|
7
|
+
- What it does
|
8
|
+
- Why it's important
|
9
|
+
- How it works at a high level]
|
10
|
+
|
11
|
+
# User Experience
|
12
|
+
[Describe the user journey and experience. Include:
|
13
|
+
- User personas
|
14
|
+
- Key user flows
|
15
|
+
- UI/UX considerations]
|
16
|
+
</context>
|
17
|
+
<PRD>
|
18
|
+
# Technical Architecture
|
19
|
+
[Outline the technical implementation details:
|
20
|
+
- System components
|
21
|
+
- Data models
|
22
|
+
- APIs and integrations
|
23
|
+
- Infrastructure requirements]
|
24
|
+
|
25
|
+
# Development Roadmap
|
26
|
+
[Break down the development process into phases:
|
27
|
+
- MVP requirements
|
28
|
+
- Future enhancements
|
29
|
+
- Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks]
|
30
|
+
|
31
|
+
# Logical Dependency Chain
|
32
|
+
[Define the logical order of development:
|
33
|
+
- Which features need to be built first (foundation)
|
34
|
+
- Getting as quickly as possible to something usable/visible front end that works
|
35
|
+
- Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches]
|
36
|
+
|
37
|
+
# Risks and Mitigations
|
38
|
+
[Identify potential risks and how they'll be addressed:
|
39
|
+
- Technical challenges
|
40
|
+
- Figuring out the MVP that we can build upon
|
41
|
+
- Resource constraints]
|
42
|
+
|
43
|
+
# Appendix
|
44
|
+
[Include any additional information:
|
45
|
+
- Research findings
|
46
|
+
- Technical specifications]
|
47
|
+
</PRD>
|
data/scripts/prd.txt
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
# AnPost API Ruby Gem PRD
|
2
|
+
|
3
|
+
## Overview
|
4
|
+
The AnPost API Ruby gem provides a simple interface to interact with An Post's API services for return label creation and tracking information retrieval. The gem should support both production and test environments.
|
5
|
+
|
6
|
+
## Core Requirements
|
7
|
+
|
8
|
+
### Configuration
|
9
|
+
- Simple configuration system with:
|
10
|
+
- test/production environment toggle
|
11
|
+
- HTTP proxy configuration (optional):
|
12
|
+
- proxy_host: Host of the proxy server
|
13
|
+
- proxy_port: Port number
|
14
|
+
- proxy_username: Optional proxy authentication username
|
15
|
+
- proxy_password: Optional proxy authentication password
|
16
|
+
- Base URL determined by environment:
|
17
|
+
- Test: https://apim-anpost-mailslabels-nonprod.dev-anpost.com/returnsapi-q/v2/
|
18
|
+
- Production: https://apim-anpost-mailslabels.anpost.com/returnsapi/v2/
|
19
|
+
|
20
|
+
### API Client
|
21
|
+
- Clean, minimal HTTP client implementation using Faraday
|
22
|
+
- Default JSON headers
|
23
|
+
- Subscription key passed per-request, not in configuration
|
24
|
+
- Proper error handling for API responses
|
25
|
+
- Support for HTTP proxy configuration from global settings
|
26
|
+
|
27
|
+
### Resources
|
28
|
+
1. Return Label Resource
|
29
|
+
- Create return labels with subscription key authentication
|
30
|
+
- Handle all necessary parameters for label creation
|
31
|
+
- Support JSON request/response formats
|
32
|
+
|
33
|
+
2. Tracking Resource
|
34
|
+
- Implement SFTP client based on existing implementation in sftp_test.rb:
|
35
|
+
- Support proxy configuration from global settings
|
36
|
+
- Connection management (connect/disconnect)
|
37
|
+
- File operations (read_file, list_files)
|
38
|
+
- Error handling for SFTP operations
|
39
|
+
|
40
|
+
- Implement tracking parser based on existing implementation:
|
41
|
+
- Parse tracking file format (header, data records, footer)
|
42
|
+
- Support both '+' and ',' delimiters
|
43
|
+
- Handle quoted fields
|
44
|
+
- Structured data output with proper field mapping
|
45
|
+
|
46
|
+
- Features:
|
47
|
+
- List available tracking files with glob pattern support
|
48
|
+
- Download and parse tracking files
|
49
|
+
- Stream processing for large files
|
50
|
+
- Proper cleanup of temporary files
|
51
|
+
- Support for incremental file processing
|
52
|
+
- Status tracking and aggregation
|
53
|
+
|
54
|
+
## Technical Requirements
|
55
|
+
- Ruby >= 3.0.0
|
56
|
+
- Dependencies:
|
57
|
+
- faraday for HTTP requests
|
58
|
+
- net-sftp for SFTP operations
|
59
|
+
- net-ssh-http-proxy for SFTP proxy support
|
60
|
+
- json for data handling
|
61
|
+
- csv for tracking file parsing
|
62
|
+
- Proper error classes and handling
|
63
|
+
- Comprehensive test coverage
|
64
|
+
- Standard Ruby code style
|
65
|
+
|
66
|
+
## Implementation Notes
|
67
|
+
- Keep configuration minimal and focused
|
68
|
+
- Move authentication to request level
|
69
|
+
- Support environment-based URL switching
|
70
|
+
- Handle errors gracefully with descriptive messages
|
71
|
+
- Follow Ruby gem best practices
|
72
|
+
- Reuse existing SFTP and parsing code from sftp_test.rb
|
73
|
+
- Ensure proper resource cleanup
|
74
|
+
- Support proxy configuration across all components
|
75
|
+
|
76
|
+
## SFTP Implementation Details
|
77
|
+
1. SFTP Client Class:
|
78
|
+
```ruby
|
79
|
+
module AnpostAPI
|
80
|
+
module SFTP
|
81
|
+
class Client
|
82
|
+
def initialize(host:, username:, password:, config: AnpostAPI.configuration)
|
83
|
+
# Initialize with credentials and use proxy from global config if available
|
84
|
+
end
|
85
|
+
|
86
|
+
def connect
|
87
|
+
# Establish SFTP connection with proxy support
|
88
|
+
end
|
89
|
+
|
90
|
+
def disconnect
|
91
|
+
# Clean disconnect
|
92
|
+
end
|
93
|
+
|
94
|
+
def read_file(remote_path, &block)
|
95
|
+
# Download and process file
|
96
|
+
end
|
97
|
+
|
98
|
+
def list_files(remote_path, glob_pattern = nil)
|
99
|
+
# List available files
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
```
|
105
|
+
|
106
|
+
2. Tracking Parser Class:
|
107
|
+
```ruby
|
108
|
+
module AnpostAPI
|
109
|
+
module SFTP
|
110
|
+
class TrackingParser
|
111
|
+
def initialize(file_path)
|
112
|
+
# Initialize with file path
|
113
|
+
end
|
114
|
+
|
115
|
+
def parse
|
116
|
+
# Parse file contents
|
117
|
+
# Return structured data with header, records, and footer
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
```
|
123
|
+
|
124
|
+
3. Example Usage:
|
125
|
+
```ruby
|
126
|
+
client = AnpostAPI::Client.new
|
127
|
+
|
128
|
+
# Configure proxy if needed
|
129
|
+
AnpostAPI.configure do |config|
|
130
|
+
config.proxy_host = "proxy.example.com"
|
131
|
+
config.proxy_port = 3128
|
132
|
+
config.proxy_username = "user"
|
133
|
+
config.proxy_password = "pass"
|
134
|
+
end
|
135
|
+
|
136
|
+
# Use tracking resource
|
137
|
+
client.tracking.get_updates(last_file: "cdt0379554008300.txt") do |filename, data|
|
138
|
+
# Process tracking data
|
139
|
+
puts "Processing file: #{filename}"
|
140
|
+
puts "Data: #{data}"
|
141
|
+
end
|
142
|
+
```
|
143
|
+
|
144
|
+
## Implementation Guidelines
|
145
|
+
|
146
|
+
### 1. Code Structure
|
147
|
+
```
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require "net/sftp"
|
2
|
+
require "pry"
|
3
|
+
require "uri"
|
4
|
+
require "net/ssh/proxy/http"
|
5
|
+
require "x25519"
|
6
|
+
require "tempfile"
|
7
|
+
require "csv"
|
8
|
+
|
9
|
+
class SFTPClient
|
10
|
+
def initialize(host, user, password, http_proxy_config = nil)
|
11
|
+
@host = host
|
12
|
+
@user = user
|
13
|
+
@password = password
|
14
|
+
@http_proxy_config = http_proxy_config
|
15
|
+
end
|
16
|
+
|
17
|
+
def connect
|
18
|
+
sftp_client.connect!
|
19
|
+
rescue Net::SSH::Exception => e
|
20
|
+
puts "Failed to connect to #{@host}"
|
21
|
+
puts e.message
|
22
|
+
end
|
23
|
+
|
24
|
+
def disconnect
|
25
|
+
sftp_client.close_channel
|
26
|
+
ssh_session.close
|
27
|
+
end
|
28
|
+
|
29
|
+
def read_file(remote_path, &block)
|
30
|
+
temp_file = Tempfile.new(["sftp", File.extname(remote_path)])
|
31
|
+
begin
|
32
|
+
@sftp_client.download!(remote_path, temp_file.path)
|
33
|
+
block_given? ? block.call(temp_file) : temp_file
|
34
|
+
rescue Net::SFTP::StatusException => e
|
35
|
+
puts "Failed to download #{remote_path}: #{e.message}"
|
36
|
+
nil
|
37
|
+
ensure
|
38
|
+
temp_file.close
|
39
|
+
temp_file.unlink
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def list_files(remote_path, glob_pattern = nil)
|
44
|
+
if glob_pattern
|
45
|
+
@sftp_client.dir.glob(remote_path, glob_pattern).each { |entry| puts entry.longname }
|
46
|
+
else
|
47
|
+
@sftp_client.dir.foreach(remote_path) { |entry| puts entry.longname }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def sftp_client
|
52
|
+
@sftp_client ||= Net::SFTP::Session.new(ssh_session)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def ssh_session
|
58
|
+
ssh_options = { password: @password, auth_methods: ["password"] }
|
59
|
+
|
60
|
+
# Add proxy configuration if provided
|
61
|
+
if @http_proxy_config
|
62
|
+
ssh_options[:proxy] = Net::SSH::Proxy::HTTP.new(
|
63
|
+
@http_proxy_config[:host],
|
64
|
+
@http_proxy_config[:port],
|
65
|
+
user: @http_proxy_config[:username],
|
66
|
+
password: @http_proxy_config[:password],
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
@ssh_session ||= Net::SSH.start(@host, @user, ssh_options)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
class TrackingDataParser
|
75
|
+
def initialize(file_path)
|
76
|
+
@file_path = file_path
|
77
|
+
end
|
78
|
+
|
79
|
+
def parse
|
80
|
+
result = { header: nil, data: [], footer: nil }
|
81
|
+
|
82
|
+
File.foreach(@file_path) do |line|
|
83
|
+
line = line.strip
|
84
|
+
next if line.empty?
|
85
|
+
|
86
|
+
# Determine delimiter and parse with CSV to handle quoted fields
|
87
|
+
delimiter = line.include?("+") ? "+" : ","
|
88
|
+
fields = CSV.parse_line(line, col_sep: delimiter, quote_char: '"')
|
89
|
+
record_type = fields[0]
|
90
|
+
|
91
|
+
case record_type
|
92
|
+
when "00"
|
93
|
+
result[:header] = parse_header(fields)
|
94
|
+
when "01"
|
95
|
+
result[:data] << parse_data_record(fields)
|
96
|
+
when "99"
|
97
|
+
result[:footer] = parse_footer(fields)
|
98
|
+
else
|
99
|
+
puts "Warning: Unknown record type: #{record_type}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
result
|
104
|
+
end
|
105
|
+
|
106
|
+
private
|
107
|
+
|
108
|
+
def parse_header(fields)
|
109
|
+
{ record_type: fields[0], file_id: fields[1], timestamp: fields[2], record_count: fields[3].to_i }
|
110
|
+
end
|
111
|
+
|
112
|
+
def parse_data_record(fields)
|
113
|
+
{
|
114
|
+
record_type: fields[0],
|
115
|
+
service: fields[1],
|
116
|
+
tracking_number: fields[2],
|
117
|
+
country: fields[3],
|
118
|
+
combined_id: fields[4],
|
119
|
+
status: fields[5],
|
120
|
+
timestamp: fields[6],
|
121
|
+
location: fields[7],
|
122
|
+
notes: fields[8],
|
123
|
+
additional_info: fields[9],
|
124
|
+
outcome: fields[10],
|
125
|
+
recipient: fields[11],
|
126
|
+
extra1: fields[12],
|
127
|
+
extra2: fields[13],
|
128
|
+
}
|
129
|
+
end
|
130
|
+
|
131
|
+
def parse_footer(fields)
|
132
|
+
{ record_type: fields[0], record_count: fields[1].to_i }
|
133
|
+
end
|
134
|
+
|
135
|
+
def format_timestamp(timestamp)
|
136
|
+
return nil if timestamp.nil?
|
137
|
+
|
138
|
+
if timestamp.length >= 14
|
139
|
+
year = timestamp[0..3]
|
140
|
+
month = timestamp[4..5]
|
141
|
+
day = timestamp[6..7]
|
142
|
+
hour = timestamp[8..9]
|
143
|
+
minute = timestamp[10..11]
|
144
|
+
second = timestamp[12..13]
|
145
|
+
|
146
|
+
"#{year}-#{month}-#{day} #{hour}:#{minute}:#{second}"
|
147
|
+
else
|
148
|
+
timestamp
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
# Example proxy configuration
|
154
|
+
http_proxy_config = {
|
155
|
+
host: "sgp-forward-proxy.postco.co",
|
156
|
+
port: 3128,
|
157
|
+
username: "proxyuser", # Optional for HTTP
|
158
|
+
password: "VG17nL@qYgAFts", # Optional for HTTP
|
159
|
+
}
|
160
|
+
|
161
|
+
start_time = Time.now
|
162
|
+
# With proxy
|
163
|
+
sftp = SFTPClient.new("anpost.moveitcloud.eu", "ctuserpostco", "nUVG<akG[4@vc}^o", http_proxy_config)
|
164
|
+
sftp.connect
|
165
|
+
end_time = Time.now
|
166
|
+
puts "Time taken to connect: #{end_time - start_time} seconds"
|
167
|
+
|
168
|
+
# Rest of your code remains the same
|
169
|
+
# sftp.list_files("/home/ctuserpostco", "cdt03795540083[4-9]*.txt")
|
170
|
+
|
171
|
+
start = 379_554_008_300
|
172
|
+
statuses = Set.new
|
173
|
+
# increment by 1 for 10 times
|
174
|
+
100.times do
|
175
|
+
file_name = "cdt0#{start += 1}.txt"
|
176
|
+
sftp.read_file("#{file_name}") do |temp_file|
|
177
|
+
parser = TrackingDataParser.new(temp_file.path)
|
178
|
+
data = parser.parse
|
179
|
+
data[:data].each { |record| statuses.add(record[:status]) }
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
puts "Statuses: #{statuses.size}"
|
184
|
+
|
185
|
+
sftp.disconnect
|