sim-meter-au 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/LICENSE.md +21 -0
- data/README.md +243 -0
- data/exe/sim-meter-au +7 -0
- data/lib/sim-meter-au.rb +4 -0
- data/lib/sim_meter_au/cli.rb +416 -0
- data/lib/sim_meter_au/errors.rb +12 -0
- data/lib/sim_meter_au/models.rb +83 -0
- data/lib/sim_meter_au/provider.rb +82 -0
- data/lib/sim_meter_au/providers/aldi_mobile/parsers/orders.rb +73 -0
- data/lib/sim_meter_au/providers/aldi_mobile/parsers/service_details.rb +140 -0
- data/lib/sim_meter_au/providers/aldi_mobile/parsers/usage.rb +83 -0
- data/lib/sim_meter_au/providers/aldi_mobile.rb +228 -0
- data/lib/sim_meter_au/serializable.rb +28 -0
- data/lib/sim_meter_au/service.rb +7 -0
- data/lib/sim_meter_au/version.rb +5 -0
- data/lib/sim_meter_au.rb +47 -0
- metadata +131 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: e71372ec0601bc9629010ed55c2a980570d729cac3f8919ffb4899a689231a28
|
|
4
|
+
data.tar.gz: a00ea1d08ab83fd4e61bba171de9d5565ca71da34e276e246b7c68cb8b49de45
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c9cf1f8112060047adff81045cceb2673c61963743d2800852760bafdc46c35feaa87b395cb94134bf5a0a2757d0952ff5a001b9fb339cad26b79f82bfa05024
|
|
7
|
+
data.tar.gz: 31773f757c185ead61ecaf975b7b37df4b15d844a3b22c9b175e830c10aebe65b740495c8de15d78ee693bb9df985757a729a4c2b9fbeb744c54cd2051d8d695
|
data/LICENSE.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Darren Jeacocke
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
# Sim Meter AU
|
|
2
|
+
|
|
3
|
+
Sim Meter AU provides a unified Ruby API for retrieving services, plan details,
|
|
4
|
+
data balances, expiry dates and usage from Australian mobile providers.
|
|
5
|
+
|
|
6
|
+
It currently supports ALDImobile through authenticated HTTP requests and
|
|
7
|
+
HTML/JSON parsing. It does not execute the provider portal's JavaScript and does
|
|
8
|
+
not require a web browser.
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
Add the gem to your Gemfile:
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
gem "sim-meter-au"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Then install it:
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
bundle install
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Providers
|
|
25
|
+
|
|
26
|
+
The currently available provider is:
|
|
27
|
+
|
|
28
|
+
- `:aldi_mobile` — ALDImobile
|
|
29
|
+
|
|
30
|
+
Every provider inherits from `SimMeterAU::Provider` and implements the same
|
|
31
|
+
operations: authentication, services, service details, usage and orders. This
|
|
32
|
+
keeps application code consistent as additional Australian providers are
|
|
33
|
+
added.
|
|
34
|
+
|
|
35
|
+
Use `SimMeterAU.providers` to get the registered provider names.
|
|
36
|
+
|
|
37
|
+
## Ruby API
|
|
38
|
+
|
|
39
|
+
Create a client by selecting a provider and supplying that provider's
|
|
40
|
+
credentials:
|
|
41
|
+
|
|
42
|
+
```ruby
|
|
43
|
+
require "sim_meter_au"
|
|
44
|
+
|
|
45
|
+
client = SimMeterAU.client(
|
|
46
|
+
provider: :aldi_mobile,
|
|
47
|
+
login: ENV.fetch("ALDI_MOBILE_LOGIN"),
|
|
48
|
+
password: ENV.fetch("ALDI_MOBILE_PASSWORD")
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
client.authenticate!
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Services
|
|
55
|
+
|
|
56
|
+
List every service on the account:
|
|
57
|
+
|
|
58
|
+
```ruby
|
|
59
|
+
services = client.services
|
|
60
|
+
services.map(&:to_h)
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
```ruby
|
|
64
|
+
[
|
|
65
|
+
{id: "1234567", name: "Data SIM", number: "0400000001"},
|
|
66
|
+
{id: "7654321", name: "Camera", number: "0400000002"}
|
|
67
|
+
]
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Service details
|
|
71
|
+
|
|
72
|
+
A service can be selected by its provider ID, mobile number or unique nickname:
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
details = client.service_details("0400000001")
|
|
76
|
+
details.to_h
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
{
|
|
81
|
+
service: {
|
|
82
|
+
id: "1234567",
|
|
83
|
+
name: "Data SIM",
|
|
84
|
+
number: "0400000001"
|
|
85
|
+
},
|
|
86
|
+
status: "active_plan",
|
|
87
|
+
plan: {
|
|
88
|
+
name: "$240 Data Plan",
|
|
89
|
+
type: "data",
|
|
90
|
+
expires_on: "2027-04-28",
|
|
91
|
+
days_remaining: 246,
|
|
92
|
+
auto_recharge: false
|
|
93
|
+
},
|
|
94
|
+
data: {
|
|
95
|
+
plan_remaining_bytes: 176_093_659_136,
|
|
96
|
+
rollover_bytes: 0,
|
|
97
|
+
total_remaining_bytes: 176_093_659_136
|
|
98
|
+
},
|
|
99
|
+
payg: {
|
|
100
|
+
balance_cents: nil,
|
|
101
|
+
expires_on: "2027-05-01",
|
|
102
|
+
days_until_expiry: 249
|
|
103
|
+
},
|
|
104
|
+
roaming_enabled: false,
|
|
105
|
+
retrieved_at: "2026-08-25T05:00:00Z"
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Other available API
|
|
110
|
+
|
|
111
|
+
- `SimMeterAU.providers` lists the registered provider names.
|
|
112
|
+
- `SimMeterAU.provider_class(provider)` returns the adapter class for a provider.
|
|
113
|
+
- `client.provider` returns the selected provider name.
|
|
114
|
+
- `client.service(identifier)` finds a service by provider ID, mobile number or
|
|
115
|
+
unique nickname.
|
|
116
|
+
- `client.usage(identifier, from:, to:)` retrieves usage summaries and
|
|
117
|
+
individual usage records. Dates may be `Date` objects or ISO date strings.
|
|
118
|
+
- `client.orders(identifier)` retrieves order history, costs and transaction
|
|
119
|
+
references.
|
|
120
|
+
- `client.authenticated?` reports whether the current provider session is
|
|
121
|
+
authenticated.
|
|
122
|
+
|
|
123
|
+
## Command-line utility
|
|
124
|
+
|
|
125
|
+
The gem includes a provider-aware `sim-meter-au` command. For local development,
|
|
126
|
+
put the provider credentials in a `.env` file:
|
|
127
|
+
|
|
128
|
+
```dotenv
|
|
129
|
+
ALDI_MOBILE_LOGIN=0412345678
|
|
130
|
+
ALDI_MOBILE_PASSWORD=your-password
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Provider selection is explicit. Pass `--provider NAME` or `-p NAME` before each
|
|
134
|
+
data command. Credential environment variables follow the provider key, so a
|
|
135
|
+
future `example_mobile` adapter can declare variables such as
|
|
136
|
+
`EXAMPLE_MOBILE_LOGIN` and `EXAMPLE_MOBILE_PASSWORD`.
|
|
137
|
+
|
|
138
|
+
Run the command without arguments, or use the `providers` command, to see the
|
|
139
|
+
available providers:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
bundle exec sim-meter-au
|
|
143
|
+
bundle exec sim-meter-au providers
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
PROVIDER NAME
|
|
148
|
+
----------- ----------
|
|
149
|
+
aldi_mobile ALDImobile
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Services
|
|
153
|
+
|
|
154
|
+
```sh
|
|
155
|
+
bundle exec sim-meter-au --provider aldi_mobile services
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
```text
|
|
159
|
+
NAME NUMBER SERVICE ID
|
|
160
|
+
-------- ---------- ----------
|
|
161
|
+
Data SIM 0400000001 1234567
|
|
162
|
+
Camera 0400000002 7654321
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Service details
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
bundle exec sim-meter-au --provider aldi_mobile service 0400000001
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```text
|
|
172
|
+
Data SIM (0400000001)
|
|
173
|
+
---------------------
|
|
174
|
+
Service ID: 1234567
|
|
175
|
+
Status: active plan
|
|
176
|
+
|
|
177
|
+
Plan
|
|
178
|
+
Name: $240 Data Plan
|
|
179
|
+
Type: data
|
|
180
|
+
Expires: 28/04/2027
|
|
181
|
+
Days remaining: 246
|
|
182
|
+
Auto recharge: Disabled
|
|
183
|
+
|
|
184
|
+
Data
|
|
185
|
+
Plan remaining: 164 GB
|
|
186
|
+
Rollover: 0 B
|
|
187
|
+
Total remaining: 164 GB
|
|
188
|
+
|
|
189
|
+
PAYG and service
|
|
190
|
+
Service expires: 01/05/2027
|
|
191
|
+
Days until expiry: 249
|
|
192
|
+
|
|
193
|
+
Roaming: Disabled
|
|
194
|
+
Retrieved: 2026-08-25T15:00:00+10:00
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
There is also a `--json` option for all data commands if you need formatted
|
|
198
|
+
output.
|
|
199
|
+
|
|
200
|
+
### Available commands
|
|
201
|
+
|
|
202
|
+
```sh
|
|
203
|
+
# List available providers
|
|
204
|
+
bundle exec sim-meter-au
|
|
205
|
+
bundle exec sim-meter-au providers
|
|
206
|
+
|
|
207
|
+
# Select a provider explicitly
|
|
208
|
+
bundle exec sim-meter-au --provider aldi_mobile services
|
|
209
|
+
|
|
210
|
+
# List services
|
|
211
|
+
bundle exec sim-meter-au --provider aldi_mobile services
|
|
212
|
+
|
|
213
|
+
# Retrieve details for every service
|
|
214
|
+
bundle exec sim-meter-au --provider aldi_mobile services --details
|
|
215
|
+
|
|
216
|
+
# Retrieve details for one service
|
|
217
|
+
bundle exec sim-meter-au --provider aldi_mobile service IDENTIFIER
|
|
218
|
+
|
|
219
|
+
# Retrieve a usage summary for the last 30 days
|
|
220
|
+
bundle exec sim-meter-au --provider aldi_mobile service IDENTIFIER usage
|
|
221
|
+
|
|
222
|
+
# Query a specific usage range
|
|
223
|
+
bundle exec sim-meter-au --provider aldi_mobile service IDENTIFIER usage \
|
|
224
|
+
--from 2026-08-01 \
|
|
225
|
+
--to 2026-08-25
|
|
226
|
+
|
|
227
|
+
# Include individual usage records
|
|
228
|
+
bundle exec sim-meter-au --provider aldi_mobile service IDENTIFIER usage --details
|
|
229
|
+
|
|
230
|
+
# Retrieve order history
|
|
231
|
+
bundle exec sim-meter-au --provider aldi_mobile service IDENTIFIER orders
|
|
232
|
+
|
|
233
|
+
# Show help or the gem version
|
|
234
|
+
bundle exec sim-meter-au --help
|
|
235
|
+
bundle exec sim-meter-au --version
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Sim Meter AU is not affiliated with or endorsed by any supported provider,
|
|
239
|
+
including ALDI or Medion Australia.
|
|
240
|
+
|
|
241
|
+
## License
|
|
242
|
+
|
|
243
|
+
Sim Meter AU is available under the [MIT License](LICENSE.md).
|
data/exe/sim-meter-au
ADDED
data/lib/sim-meter-au.rb
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "date"
|
|
6
|
+
|
|
7
|
+
module SimMeterAU
|
|
8
|
+
class CLI
|
|
9
|
+
PROVIDER_COMMANDS = %w[services service].freeze
|
|
10
|
+
|
|
11
|
+
def initialize(out: $stdout, err: $stderr, env: ENV, client_factory: nil)
|
|
12
|
+
@out = out
|
|
13
|
+
@err = err
|
|
14
|
+
@env = env
|
|
15
|
+
@client_factory = client_factory || method(:build_client)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run(arguments)
|
|
19
|
+
arguments = arguments.dup
|
|
20
|
+
global_options = parse_global_options(arguments)
|
|
21
|
+
return global_options if global_options.is_a?(Integer)
|
|
22
|
+
|
|
23
|
+
@provider_name = global_options.fetch(:provider)
|
|
24
|
+
command = arguments.shift || "providers"
|
|
25
|
+
|
|
26
|
+
if PROVIDER_COMMANDS.include?(command) && @provider_name.to_s.empty?
|
|
27
|
+
raise ConfigurationError,
|
|
28
|
+
"Select a provider with --provider NAME (available: #{SimMeterAU.providers.join(", ")})"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
case command
|
|
32
|
+
when "providers"
|
|
33
|
+
list_providers(arguments)
|
|
34
|
+
when "services"
|
|
35
|
+
list_services(arguments)
|
|
36
|
+
when "service"
|
|
37
|
+
service_command(arguments)
|
|
38
|
+
when "-h", "--help"
|
|
39
|
+
@out.puts help
|
|
40
|
+
0
|
|
41
|
+
when "-v", "--version"
|
|
42
|
+
@out.puts VERSION
|
|
43
|
+
0
|
|
44
|
+
else
|
|
45
|
+
@err.puts "Unknown command: #{command}"
|
|
46
|
+
@err.puts help
|
|
47
|
+
1
|
|
48
|
+
end
|
|
49
|
+
rescue ConfigurationError, AuthenticationError, ConnectionError,
|
|
50
|
+
UnexpectedResponseError, ServiceNotFoundError, AmbiguousServiceError => error
|
|
51
|
+
@err.puts error.message
|
|
52
|
+
1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def list_providers(arguments)
|
|
58
|
+
options = {json: false}
|
|
59
|
+
parser = OptionParser.new do |opts|
|
|
60
|
+
opts.banner = "Usage: sim-meter-au providers [--json]"
|
|
61
|
+
opts.on("--json", "Print providers as JSON") { options[:json] = true }
|
|
62
|
+
end
|
|
63
|
+
parser.parse!(arguments)
|
|
64
|
+
|
|
65
|
+
providers = SimMeterAU.providers.map do |key|
|
|
66
|
+
provider_class = SimMeterAU.provider_class(key)
|
|
67
|
+
{key:, name: provider_class.display_name}
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
if options[:json]
|
|
71
|
+
@out.puts JSON.pretty_generate(providers)
|
|
72
|
+
else
|
|
73
|
+
print_table(
|
|
74
|
+
["PROVIDER", "NAME"],
|
|
75
|
+
providers.map { |provider| [provider[:key], provider[:name]] }
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
0
|
|
79
|
+
rescue OptionParser::ParseError => error
|
|
80
|
+
option_error(error, parser)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_global_options(arguments)
|
|
84
|
+
options = {
|
|
85
|
+
provider: nil
|
|
86
|
+
}
|
|
87
|
+
parser = OptionParser.new do |opts|
|
|
88
|
+
opts.banner = "Usage: sim-meter-au [options] COMMAND"
|
|
89
|
+
opts.on("-p", "--provider NAME", "Provider to use") do |name|
|
|
90
|
+
options[:provider] = name
|
|
91
|
+
end
|
|
92
|
+
opts.on("-h", "--help", "Show this help") { options[:help] = true }
|
|
93
|
+
opts.on("-v", "--version", "Show the gem version") { options[:version] = true }
|
|
94
|
+
end
|
|
95
|
+
parser.order!(arguments)
|
|
96
|
+
if options[:help]
|
|
97
|
+
@out.puts help
|
|
98
|
+
return 0
|
|
99
|
+
end
|
|
100
|
+
if options[:version]
|
|
101
|
+
@out.puts VERSION
|
|
102
|
+
return 0
|
|
103
|
+
end
|
|
104
|
+
options
|
|
105
|
+
rescue OptionParser::ParseError => error
|
|
106
|
+
@err.puts error.message
|
|
107
|
+
@err.puts parser
|
|
108
|
+
1
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def list_services(arguments)
|
|
112
|
+
options = {json: false, details: false}
|
|
113
|
+
parser = OptionParser.new do |opts|
|
|
114
|
+
opts.banner = "Usage: sim-meter-au services [--details] [--json]"
|
|
115
|
+
opts.on("--details", "Retrieve plan and balance details") { options[:details] = true }
|
|
116
|
+
opts.on("--json", "Print services as JSON") { options[:json] = true }
|
|
117
|
+
end
|
|
118
|
+
parser.parse!(arguments)
|
|
119
|
+
|
|
120
|
+
client.authenticate!
|
|
121
|
+
services = client.services
|
|
122
|
+
results = if options[:details]
|
|
123
|
+
services.map { |service| client.service_details(service) }
|
|
124
|
+
else
|
|
125
|
+
services
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
if options[:json]
|
|
129
|
+
@out.puts JSON.pretty_generate(results.map(&:to_h))
|
|
130
|
+
elsif options[:details]
|
|
131
|
+
results.each_with_index do |details, index|
|
|
132
|
+
@out.puts if index.positive?
|
|
133
|
+
print_service_details(details)
|
|
134
|
+
end
|
|
135
|
+
else
|
|
136
|
+
print_service_table(services)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
0
|
|
140
|
+
rescue OptionParser::ParseError => error
|
|
141
|
+
@err.puts error.message
|
|
142
|
+
@err.puts parser
|
|
143
|
+
1
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def service_command(arguments)
|
|
147
|
+
identifier = arguments.shift
|
|
148
|
+
raise ConfigurationError, "Usage: sim-meter-au service IDENTIFIER [usage|orders]" if identifier.to_s.empty?
|
|
149
|
+
|
|
150
|
+
subcommand = arguments.first
|
|
151
|
+
case subcommand
|
|
152
|
+
when "usage"
|
|
153
|
+
arguments.shift
|
|
154
|
+
service_usage(identifier, arguments)
|
|
155
|
+
when "orders"
|
|
156
|
+
arguments.shift
|
|
157
|
+
service_orders(identifier, arguments)
|
|
158
|
+
else
|
|
159
|
+
service_details(identifier, arguments)
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def service_details(identifier, arguments)
|
|
164
|
+
options = {json: false}
|
|
165
|
+
parser = OptionParser.new do |opts|
|
|
166
|
+
opts.banner = "Usage: sim-meter-au service IDENTIFIER [--json]"
|
|
167
|
+
opts.on("--json", "Print details as JSON") { options[:json] = true }
|
|
168
|
+
end
|
|
169
|
+
parser.parse!(arguments)
|
|
170
|
+
|
|
171
|
+
client.authenticate!
|
|
172
|
+
details = client.service_details(identifier)
|
|
173
|
+
|
|
174
|
+
if options[:json]
|
|
175
|
+
@out.puts JSON.pretty_generate(details.to_h)
|
|
176
|
+
else
|
|
177
|
+
print_service_details(details)
|
|
178
|
+
end
|
|
179
|
+
0
|
|
180
|
+
rescue OptionParser::ParseError => error
|
|
181
|
+
option_error(error, parser)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def service_usage(identifier, arguments)
|
|
185
|
+
options = {
|
|
186
|
+
json: false,
|
|
187
|
+
details: false,
|
|
188
|
+
from: Date.today - 30,
|
|
189
|
+
to: Date.today
|
|
190
|
+
}
|
|
191
|
+
parser = OptionParser.new do |opts|
|
|
192
|
+
opts.banner = "Usage: sim-meter-au service IDENTIFIER usage [options]"
|
|
193
|
+
opts.on("--from DATE", "Start date in YYYY-MM-DD format") { |date| options[:from] = parse_date(date) }
|
|
194
|
+
opts.on("--to DATE", "End date in YYYY-MM-DD format") { |date| options[:to] = parse_date(date) }
|
|
195
|
+
opts.on("--details", "Include individual usage records") { options[:details] = true }
|
|
196
|
+
opts.on("--json", "Print usage as JSON") { options[:json] = true }
|
|
197
|
+
end
|
|
198
|
+
parser.parse!(arguments)
|
|
199
|
+
|
|
200
|
+
client.authenticate!
|
|
201
|
+
report = client.usage(identifier, from: options[:from], to: options[:to])
|
|
202
|
+
|
|
203
|
+
if options[:json]
|
|
204
|
+
payload = report.to_h
|
|
205
|
+
payload[:records] = [] unless options[:details]
|
|
206
|
+
payload[:record_count] = report.records.length
|
|
207
|
+
@out.puts JSON.pretty_generate(payload)
|
|
208
|
+
else
|
|
209
|
+
print_usage(report, include_records: options[:details])
|
|
210
|
+
end
|
|
211
|
+
0
|
|
212
|
+
rescue OptionParser::ParseError => error
|
|
213
|
+
option_error(error, parser)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def service_orders(identifier, arguments)
|
|
217
|
+
options = {json: false}
|
|
218
|
+
parser = OptionParser.new do |opts|
|
|
219
|
+
opts.banner = "Usage: sim-meter-au service IDENTIFIER orders [--json]"
|
|
220
|
+
opts.on("--json", "Print orders as JSON") { options[:json] = true }
|
|
221
|
+
end
|
|
222
|
+
parser.parse!(arguments)
|
|
223
|
+
|
|
224
|
+
client.authenticate!
|
|
225
|
+
orders = client.orders(identifier)
|
|
226
|
+
|
|
227
|
+
if options[:json]
|
|
228
|
+
@out.puts JSON.pretty_generate(orders.map(&:to_h))
|
|
229
|
+
else
|
|
230
|
+
print_orders(orders)
|
|
231
|
+
end
|
|
232
|
+
0
|
|
233
|
+
rescue OptionParser::ParseError => error
|
|
234
|
+
option_error(error, parser)
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def print_service_table(services)
|
|
238
|
+
if services.empty?
|
|
239
|
+
@out.puts "No services found."
|
|
240
|
+
return
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
print_table(
|
|
244
|
+
["NAME", "NUMBER", "SERVICE ID"],
|
|
245
|
+
services.map { |service| [service.name, service.number, service.id] }
|
|
246
|
+
)
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def print_service_details(details)
|
|
250
|
+
service = details.service
|
|
251
|
+
@out.puts "#{service.name} (#{service.number})"
|
|
252
|
+
@out.puts "-" * (service.name.length + service.number.length + 3)
|
|
253
|
+
print_value("Service ID", service.id)
|
|
254
|
+
print_value("Status", details.status.tr("_", " "))
|
|
255
|
+
|
|
256
|
+
if details.plan
|
|
257
|
+
@out.puts
|
|
258
|
+
@out.puts "Plan"
|
|
259
|
+
print_value("Name", details.plan.name)
|
|
260
|
+
print_value("Type", details.plan.type)
|
|
261
|
+
print_value("Expires", format_date(details.plan.expires_on))
|
|
262
|
+
print_value("Days remaining", details.plan.days_remaining)
|
|
263
|
+
print_value("Auto recharge", format_boolean(details.plan.auto_recharge))
|
|
264
|
+
else
|
|
265
|
+
@out.puts
|
|
266
|
+
print_value("Plan", "None")
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
if details.data
|
|
270
|
+
@out.puts
|
|
271
|
+
@out.puts "Data"
|
|
272
|
+
print_value("Plan remaining", format_bytes(details.data.plan_remaining_bytes))
|
|
273
|
+
print_value("Rollover", format_bytes(details.data.rollover_bytes))
|
|
274
|
+
print_value("Total remaining", format_bytes(details.data.total_remaining_bytes))
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
if details.payg
|
|
278
|
+
@out.puts
|
|
279
|
+
@out.puts "PAYG and service"
|
|
280
|
+
print_value("Balance", format_cents(details.payg.balance_cents))
|
|
281
|
+
print_value("Service expires", format_date(details.payg.expires_on))
|
|
282
|
+
print_value("Days until expiry", details.payg.days_until_expiry)
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
@out.puts
|
|
286
|
+
print_value("Roaming", format_boolean(details.roaming_enabled))
|
|
287
|
+
print_value("Retrieved", details.retrieved_at.iso8601)
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def print_usage(report, include_records:)
|
|
291
|
+
@out.puts "Usage for #{report.service.name} (#{report.service.number})"
|
|
292
|
+
@out.puts "#{report.from.iso8601} to #{report.to.iso8601}"
|
|
293
|
+
@out.puts
|
|
294
|
+
|
|
295
|
+
summary_rows = report.summaries.map do |summary|
|
|
296
|
+
[summary.type, summary.count.to_s, summary.rounded_usage, "$#{summary.charge_dollars}"]
|
|
297
|
+
end
|
|
298
|
+
print_table(["TYPE", "COUNT", "ROUNDED USAGE", "CHARGE"], summary_rows)
|
|
299
|
+
@out.puts "No usage found." if summary_rows.empty?
|
|
300
|
+
@out.puts "Individual records: #{report.records.length}"
|
|
301
|
+
|
|
302
|
+
return unless include_records && report.records.any?
|
|
303
|
+
|
|
304
|
+
@out.puts
|
|
305
|
+
rows = report.records.map do |record|
|
|
306
|
+
[record.type.to_s, record.destination, record.usage, "$#{record.charge_dollars}", record.date_label]
|
|
307
|
+
end
|
|
308
|
+
print_table(["TYPE", "DESTINATION", "USAGE", "CHARGE", "DATE"], rows)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def print_orders(orders)
|
|
312
|
+
if orders.empty?
|
|
313
|
+
@out.puts "No orders found."
|
|
314
|
+
return
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
rows = orders.map do |order|
|
|
318
|
+
[order.id, order.type, order.plan_name, format_cents(order.total_cents), order.date_label]
|
|
319
|
+
end
|
|
320
|
+
print_table(["ORDER", "TYPE", "PLAN", "TOTAL", "DATE"], rows)
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def print_table(headers, rows)
|
|
324
|
+
all_rows = [headers] + rows.map { |row| row.map(&:to_s) }
|
|
325
|
+
widths = all_rows.transpose.map { |column| column.map(&:length).max }
|
|
326
|
+
|
|
327
|
+
all_rows.each_with_index do |row, index|
|
|
328
|
+
@out.puts row.each_with_index.map { |value, column| value.ljust(widths[column]) }.join(" ").rstrip
|
|
329
|
+
@out.puts widths.map { |width| "-" * width }.join(" ") if index.zero?
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
def print_value(label, value)
|
|
334
|
+
return if value.nil?
|
|
335
|
+
|
|
336
|
+
@out.puts format("%-18s %s", "#{label}:", value)
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def format_bytes(bytes)
|
|
340
|
+
return if bytes.nil?
|
|
341
|
+
return "0 B" if bytes.zero?
|
|
342
|
+
|
|
343
|
+
units = %w[B KB MB GB TB]
|
|
344
|
+
value = bytes.to_f
|
|
345
|
+
unit = units.shift
|
|
346
|
+
while value >= 1024 && units.any?
|
|
347
|
+
value /= 1024
|
|
348
|
+
unit = units.shift
|
|
349
|
+
end
|
|
350
|
+
precision = value >= 100 ? 0 : value >= 10 ? 1 : 2
|
|
351
|
+
"#{format("%.#{precision}f", value)} #{unit}"
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def format_cents(cents)
|
|
355
|
+
return if cents.nil?
|
|
356
|
+
|
|
357
|
+
format("$%.2f", cents / 100.0)
|
|
358
|
+
end
|
|
359
|
+
|
|
360
|
+
def format_date(date)
|
|
361
|
+
date&.strftime("%d/%m/%Y")
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def format_boolean(value)
|
|
365
|
+
return "Unknown" if value.nil?
|
|
366
|
+
|
|
367
|
+
value ? "Enabled" : "Disabled"
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def parse_date(value)
|
|
371
|
+
Date.iso8601(value)
|
|
372
|
+
rescue Date::Error
|
|
373
|
+
raise OptionParser::InvalidArgument, "invalid date: #{value}"
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def option_error(error, parser)
|
|
377
|
+
@err.puts error.message
|
|
378
|
+
@err.puts parser
|
|
379
|
+
1
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def client
|
|
383
|
+
@client ||= @client_factory.call(provider: @provider_name, **credentials)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def credentials
|
|
387
|
+
SimMeterAU.provider_class(@provider_name).credentials_from(@env)
|
|
388
|
+
end
|
|
389
|
+
|
|
390
|
+
def build_client(provider:, **credentials)
|
|
391
|
+
SimMeterAU.client(provider:, **credentials)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def help
|
|
395
|
+
<<~HELP
|
|
396
|
+
Usage: sim-meter-au [options] COMMAND
|
|
397
|
+
|
|
398
|
+
Provider:
|
|
399
|
+
-p, --provider NAME Select a provider (required for data commands)
|
|
400
|
+
|
|
401
|
+
Commands:
|
|
402
|
+
providers List available providers (default command)
|
|
403
|
+
services List services on the account
|
|
404
|
+
services --details
|
|
405
|
+
List details for every service
|
|
406
|
+
service ID Show plan, data, expiry and roaming details
|
|
407
|
+
service ID usage Show usage for a date range
|
|
408
|
+
service ID orders Show order history
|
|
409
|
+
|
|
410
|
+
Options:
|
|
411
|
+
-h, --help Show this help
|
|
412
|
+
-v, --version Show the gem version
|
|
413
|
+
HELP
|
|
414
|
+
end
|
|
415
|
+
end
|
|
416
|
+
end
|