Skip to content

Commit 3e150f4

Browse files
authored
Add cli app (#32)
1 parent ddecb4a commit 3e150f4

File tree

9 files changed

+154
-6
lines changed

9 files changed

+154
-6
lines changed

skyetel/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,6 @@ FROM public.ecr.aws/lambda/ruby:$RUBY_VERSION
2828
COPY --from=build-image ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT}
2929

3030
ENV RUBY_YJIT_ENABLE=true
31+
ENV APP_ENV=production
3132

3233
CMD [ "app.App::Handler.process" ]

skyetel/Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ source "https://rubygems.org"
33
gem "aws-sdk-ssm"
44
gem "csv"
55
gem "encrypted_credentials", github: "somleng/encrypted_credentials"
6+
gem "logger"
67
gem "faraday"
78
gem "rack"
89
gem "rate_center"

skyetel/Gemfile.lock

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ DEPENDENCIES
146146
csv
147147
encrypted_credentials!
148148
faraday
149+
logger
149150
pry
150151
rack
151152
rake

skyetel/README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ This integration runs on a schedule and automatically orders DIDs from [Skyetel]
88

99
| Variable | Description | Example | Required | Default |
1010
| -------------------------- | --------------------------------------------------------- | ---------------------- | -------- | ---------------------- |
11+
| APP_ENV | Application environment | production | false | production |
1112
| SOMLENG_API_KEY | Somleng Carrier API Key SID | change-me | true | none |
1213
| SKYETEL_USERNAME | Skyetel API Username Token | change-me | true | none |
1314
| SKYETEL_PASSWORD | Skyetel API Password | change-me | true | none |
@@ -20,6 +21,14 @@ This integration runs on a schedule and automatically orders DIDs from [Skyetel]
2021

2122
See [examples](https://github.com/somleng/somleng-integrations/tree/develop/skyetel/examples).
2223

24+
## CLI
25+
26+
The CLI can be used to test your integration or in standalone mode.
27+
28+
```bash
29+
./bin/somleng-skyetel
30+
```
31+
2332
## Deployment
2433

2534
The [docker image](https://github.com/somleng/somleng-integrations/pkgs/container/somleng-skyetel) is automatically configured for deployment to AWS Lambda.
Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
class ShoppingList
22
LineItem = Struct.new(:country, :region, :locality, :quantity, :nearby_rate_centers)
33

4-
attr_reader :line_items
4+
attr_reader :line_items, :cities, :min_stock, :max_stock
55

6-
def initialize(line_items:)
7-
@line_items = Array(line_items)
6+
def initialize(**options)
7+
@line_items = Array(options.fetch(:line_items))
8+
@cities = options[:cities]
9+
@min_stock = options[:min_stock]
10+
@max_stock = options[:max_stock]
811
end
912
end

skyetel/app/workflows/generate_shopping_list.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ def call
3232
)
3333
end
3434

35-
ShoppingList.new(line_items:)
35+
ShoppingList.new(line_items:, cities:, min_stock:, max_stock:)
3636
end
3737
end

skyetel/app/workflows/restock_inventory.rb

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,93 @@ def self.call(...)
33
new(...).call
44
end
55

6-
attr_reader :somleng_client, :skyetel_client
6+
attr_reader :somleng_client, :skyetel_client, :dry_run, :logger, :verbose
77

88
def initialize(**options)
99
@somleng_client = options.fetch(:somleng_client) { Somleng::CarrierAPI::Client.new }
1010
@skyetel_client = options.fetch(:skyetel_client) { Skyetel::Client.new }
11+
@dry_run = options[:dry_run]
12+
@verbose = options[:verbose]
13+
@logger = options.fetch(:logger) { Logger.new(STDOUT) }
1114
end
1215

1316
def call
17+
inventory_report = generate_inventory_report
18+
shopping_list = generate_shopping_list(inventory_report)
19+
purchase_order = generate_purchase_order(shopping_list)
20+
execute_order(purchase_order)
21+
update_inventory(purchase_order)
22+
end
23+
24+
private
25+
26+
def generate_inventory_report
27+
logger.info("Generating inventory report...")
1428
inventory_report = GenerateInventoryReport.call(client: somleng_client)
29+
logger.info("Done.")
30+
log_inventory_report(inventory_report) if verbose
31+
inventory_report
32+
end
33+
34+
def generate_shopping_list(inventory_report)
35+
logger.info("Generating shopping list...")
1536
shopping_list = GenerateShoppingList.call(inventory_report:)
37+
logger.info("Done.")
38+
log_shopping_list(shopping_list) if verbose
39+
shopping_list
40+
end
41+
42+
def generate_purchase_order(shopping_list)
43+
logger.info("Generating purchase order...")
1644
purchase_order = GeneratePurchaseOrder.call(shopping_list:, client: skyetel_client)
45+
logger.info("Done.")
46+
logger.info("Purchase order contains #{purchase_order.to_order.count} numbers.") if verbose
47+
purchase_order
48+
end
49+
50+
def execute_order(purchase_order)
51+
if dry_run
52+
logger.info("Dry run. Skipping order execution.")
53+
return
54+
end
55+
56+
logger.info("Executing order...")
1757
ExecuteOrder.call(purchase_order:, client: skyetel_client)
58+
logger.info("Done.")
59+
end
60+
61+
def update_inventory(purchase_order)
62+
if dry_run
63+
logger.info("Dry run. Skipping inventory update.")
64+
return
65+
end
66+
67+
logger.info("Updating inventory...")
1868
UpdateInventory.call(purchase_order:, client: somleng_client)
69+
logger.info("Done.")
70+
end
71+
72+
def log_inventory_report(inventory_report)
73+
logger.info("Inventory report contains #{inventory_report.line_items.count} cities.")
74+
report_summary = inventory_report.line_items.each_with_object({}) do |line_item, result|
75+
result["#{line_item.country}/#{line_item.region}/#{line_item.locality}"] = line_item.quantity
76+
end
77+
78+
logger.info("Inventory report: #{JSON.pretty_generate(report_summary)}")
79+
end
80+
81+
def log_shopping_list(shopping_list)
82+
logger.info("Shopping list generated with the following options: MIN_STOCK: #{shopping_list.min_stock}, MAX_STOCK: #{shopping_list.max_stock}.")
83+
if shopping_list.line_items.count == 0
84+
logger.warn("Shopping list contains no items.")
85+
cities = shopping_list.cities.map { |city| "#{city.country}/#{city.region}/#{city.name}" }
86+
logger.info("Shopping list generated for the following cities: #{JSON.pretty_generate(cities)}")
87+
else
88+
logger.info("Shopping list contains #{shopping_list.line_items.count} items.")
89+
report_summary = shopping_list.line_items.each_with_object({}) do |line_item, result|
90+
result["#{line_item.country}/#{line_item.region}/#{line_item.locality}"] = line_item.quantity
91+
end
92+
logger.info("Shopping list: #{JSON.pretty_generate(report_summary)}")
93+
end
1994
end
2095
end

skyetel/bin/somleng-skyetel

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require "bundler/setup"
5+
require "optparse"
6+
require_relative "../config/application"
7+
8+
class OptionsParser
9+
class MissingArgumentError < StandardError; end
10+
11+
Options = Struct.new(:dry_run, :verbose)
12+
13+
attr_reader :parser, :options
14+
15+
def initialize(**options)
16+
@parser = options.fetch(:parser) { default_parser }
17+
@options = Options.new
18+
end
19+
20+
def parse
21+
parser.parse!
22+
check_environment!("APP_ENV", "SOMLENG_API_KEY", "SKYETEL_USERNAME", "SKYETEL_PASSWORD", "MIN_STOCK", "MAX_STOCK")
23+
options
24+
end
25+
26+
def help
27+
parser.help
28+
end
29+
30+
private
31+
32+
def check_environment!(*keys)
33+
Array(keys).each do |key|
34+
raise MissingArgumentError.new("missing env var: #{key}") unless ENV.key?(key)
35+
end
36+
end
37+
38+
def default_parser
39+
OptionParser.new do |opts|
40+
opts.banner = "Usage: somleng-skyetel [options]"
41+
opts.on("--[no-]dry-run [FLAG]", "Dry run only. No phone numbers will be actually purchased.", TrueClass) { |o| options.dry_run = o.nil? ? true : o }
42+
opts.on("--[no-]verbose [FLAG]", "Run verbosely", TrueClass) { |o| options.verbose = o.nil? ? true : o }
43+
end
44+
end
45+
end
46+
47+
def parse_options
48+
parser = OptionsParser.new
49+
parser.parse
50+
rescue OptionsParser::MissingArgumentError => e
51+
puts e.message
52+
puts parser.help
53+
exit(1)
54+
end
55+
56+
options = parse_options
57+
58+
RestockInventory.call(dry_run: options.dry_run, verbose: options.verbose)

skyetel/examples/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,5 @@ The image is ready to be deployed to AWS Lambda and can be triggered by a schedu
1717
If you're not using Lambda, you can run your image with the following command.
1818

1919
```bash
20-
docker run --platform linux/amd64 --rm -it -e APP_ENV=production -e SOMLENG_API_KEY='somleng-carrier-api-key' SOMLENG_API_KEY='somleng-carrier-api-key' -e SKYETEL_USERNAME='skyetel-username' -e SKYETEL_PASSWORD='skyetel-password' -e MIN_STOCK=2 -e MAX_STOCK=2 --entrypoint ruby somleng-skyetel:example -r ./app.rb -e App::Handler.process
20+
docker run --platform linux/amd64 --rm -it -e APP_ENV=production -e SOMLENG_API_KEY='somleng-carrier-api-key' SOMLENG_API_KEY='somleng-carrier-api-key' -e SKYETEL_USERNAME='skyetel-username' -e SKYETEL_PASSWORD='skyetel-password' -e MIN_STOCK=5 -e MAX_STOCK=10 --entrypoint ./bin/somleng-skyetel somleng-skyetel:example
2121
```

0 commit comments

Comments
 (0)