Skip to content

Commit

Permalink
Add cli app (#32)
Browse files Browse the repository at this point in the history
  • Loading branch information
dwilkie authored Oct 12, 2024
1 parent ddecb4a commit 3e150f4
Show file tree
Hide file tree
Showing 9 changed files with 154 additions and 6 deletions.
1 change: 1 addition & 0 deletions skyetel/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ FROM public.ecr.aws/lambda/ruby:$RUBY_VERSION
COPY --from=build-image ${LAMBDA_TASK_ROOT} ${LAMBDA_TASK_ROOT}

ENV RUBY_YJIT_ENABLE=true
ENV APP_ENV=production

CMD [ "app.App::Handler.process" ]
1 change: 1 addition & 0 deletions skyetel/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ source "https://rubygems.org"
gem "aws-sdk-ssm"
gem "csv"
gem "encrypted_credentials", github: "somleng/encrypted_credentials"
gem "logger"
gem "faraday"
gem "rack"
gem "rate_center"
Expand Down
1 change: 1 addition & 0 deletions skyetel/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ DEPENDENCIES
csv
encrypted_credentials!
faraday
logger
pry
rack
rake
Expand Down
9 changes: 9 additions & 0 deletions skyetel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ This integration runs on a schedule and automatically orders DIDs from [Skyetel]

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

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

## CLI

The CLI can be used to test your integration or in standalone mode.

```bash
./bin/somleng-skyetel
```

## Deployment

The [docker image](https://github.com/somleng/somleng-integrations/pkgs/container/somleng-skyetel) is automatically configured for deployment to AWS Lambda.
9 changes: 6 additions & 3 deletions skyetel/app/models/shopping_list.rb
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
class ShoppingList
LineItem = Struct.new(:country, :region, :locality, :quantity, :nearby_rate_centers)

attr_reader :line_items
attr_reader :line_items, :cities, :min_stock, :max_stock

def initialize(line_items:)
@line_items = Array(line_items)
def initialize(**options)
@line_items = Array(options.fetch(:line_items))
@cities = options[:cities]
@min_stock = options[:min_stock]
@max_stock = options[:max_stock]
end
end
2 changes: 1 addition & 1 deletion skyetel/app/workflows/generate_shopping_list.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ def call
)
end

ShoppingList.new(line_items:)
ShoppingList.new(line_items:, cities:, min_stock:, max_stock:)
end
end
77 changes: 76 additions & 1 deletion skyetel/app/workflows/restock_inventory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,93 @@ def self.call(...)
new(...).call
end

attr_reader :somleng_client, :skyetel_client
attr_reader :somleng_client, :skyetel_client, :dry_run, :logger, :verbose

def initialize(**options)
@somleng_client = options.fetch(:somleng_client) { Somleng::CarrierAPI::Client.new }
@skyetel_client = options.fetch(:skyetel_client) { Skyetel::Client.new }
@dry_run = options[:dry_run]
@verbose = options[:verbose]
@logger = options.fetch(:logger) { Logger.new(STDOUT) }
end

def call
inventory_report = generate_inventory_report
shopping_list = generate_shopping_list(inventory_report)
purchase_order = generate_purchase_order(shopping_list)
execute_order(purchase_order)
update_inventory(purchase_order)
end

private

def generate_inventory_report
logger.info("Generating inventory report...")
inventory_report = GenerateInventoryReport.call(client: somleng_client)
logger.info("Done.")
log_inventory_report(inventory_report) if verbose
inventory_report
end

def generate_shopping_list(inventory_report)
logger.info("Generating shopping list...")
shopping_list = GenerateShoppingList.call(inventory_report:)
logger.info("Done.")
log_shopping_list(shopping_list) if verbose
shopping_list
end

def generate_purchase_order(shopping_list)
logger.info("Generating purchase order...")
purchase_order = GeneratePurchaseOrder.call(shopping_list:, client: skyetel_client)
logger.info("Done.")
logger.info("Purchase order contains #{purchase_order.to_order.count} numbers.") if verbose
purchase_order
end

def execute_order(purchase_order)
if dry_run
logger.info("Dry run. Skipping order execution.")
return
end

logger.info("Executing order...")
ExecuteOrder.call(purchase_order:, client: skyetel_client)
logger.info("Done.")
end

def update_inventory(purchase_order)
if dry_run
logger.info("Dry run. Skipping inventory update.")
return
end

logger.info("Updating inventory...")
UpdateInventory.call(purchase_order:, client: somleng_client)
logger.info("Done.")
end

def log_inventory_report(inventory_report)
logger.info("Inventory report contains #{inventory_report.line_items.count} cities.")
report_summary = inventory_report.line_items.each_with_object({}) do |line_item, result|
result["#{line_item.country}/#{line_item.region}/#{line_item.locality}"] = line_item.quantity
end

logger.info("Inventory report: #{JSON.pretty_generate(report_summary)}")
end

def log_shopping_list(shopping_list)
logger.info("Shopping list generated with the following options: MIN_STOCK: #{shopping_list.min_stock}, MAX_STOCK: #{shopping_list.max_stock}.")
if shopping_list.line_items.count == 0
logger.warn("Shopping list contains no items.")
cities = shopping_list.cities.map { |city| "#{city.country}/#{city.region}/#{city.name}" }
logger.info("Shopping list generated for the following cities: #{JSON.pretty_generate(cities)}")
else
logger.info("Shopping list contains #{shopping_list.line_items.count} items.")
report_summary = shopping_list.line_items.each_with_object({}) do |line_item, result|
result["#{line_item.country}/#{line_item.region}/#{line_item.locality}"] = line_item.quantity
end
logger.info("Shopping list: #{JSON.pretty_generate(report_summary)}")
end
end
end
58 changes: 58 additions & 0 deletions skyetel/bin/somleng-skyetel
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

require "bundler/setup"
require "optparse"
require_relative "../config/application"

class OptionsParser
class MissingArgumentError < StandardError; end

Options = Struct.new(:dry_run, :verbose)

attr_reader :parser, :options

def initialize(**options)
@parser = options.fetch(:parser) { default_parser }
@options = Options.new
end

def parse
parser.parse!
check_environment!("APP_ENV", "SOMLENG_API_KEY", "SKYETEL_USERNAME", "SKYETEL_PASSWORD", "MIN_STOCK", "MAX_STOCK")
options
end

def help
parser.help
end

private

def check_environment!(*keys)
Array(keys).each do |key|
raise MissingArgumentError.new("missing env var: #{key}") unless ENV.key?(key)
end
end

def default_parser
OptionParser.new do |opts|
opts.banner = "Usage: somleng-skyetel [options]"
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 }
opts.on("--[no-]verbose [FLAG]", "Run verbosely", TrueClass) { |o| options.verbose = o.nil? ? true : o }
end
end
end

def parse_options
parser = OptionsParser.new
parser.parse
rescue OptionsParser::MissingArgumentError => e
puts e.message
puts parser.help
exit(1)
end

options = parse_options

RestockInventory.call(dry_run: options.dry_run, verbose: options.verbose)
2 changes: 1 addition & 1 deletion skyetel/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ The image is ready to be deployed to AWS Lambda and can be triggered by a schedu
If you're not using Lambda, you can run your image with the following command.

```bash
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
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
```

0 comments on commit 3e150f4

Please sign in to comment.