Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cli app #32

Merged
merged 2 commits into from
Oct 12, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
```