Skip to content
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
72 changes: 68 additions & 4 deletions cmd/archera_comparison.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ const (
var archeraHTTPClient *http.Client

var archeraOpts struct {
OrgID string
PlanID string
Format string
OrgID string
PlanID string
Format string
LineItemIDs []string
ContractTerms []string
PaymentOptions []string
}

var archeraComparisonCmd = &cobra.Command{
Expand All @@ -59,6 +62,9 @@ func init() {
archeraComparisonCmd.Flags().StringVar(&archeraOpts.OrgID, "org-id", "", "Archera organization UUID (default: $"+archeraEnvOrgID+")")
archeraComparisonCmd.Flags().StringVar(&archeraOpts.PlanID, "plan-id", "", "Archera commitment plan UUID (default: $"+archeraEnvPlanID+")")
archeraComparisonCmd.Flags().StringVar(&archeraOpts.Format, "format", "table", "Output format: table or json")
archeraComparisonCmd.Flags().StringArrayVar(&archeraOpts.LineItemIDs, "line-item-ids", nil, "Archera line-item UUIDs, comma-separated or repeated (max 200)")
archeraComparisonCmd.Flags().StringArrayVar(&archeraOpts.ContractTerms, "contract-terms", nil, fmt.Sprintf("Exact Archera contract terms, comma-separated or repeated (max %d)", len(insurance.ContractTerms())))
archeraComparisonCmd.Flags().StringArrayVar(&archeraOpts.PaymentOptions, "payment-options", nil, "Payment options: no_upfront, partial_upfront, all_upfront (comma-separated or repeated; max 3)")
}

// archeraSetting returns the flag value, else the environment value, trimmed.
Expand All @@ -79,13 +85,18 @@ func runArcheraComparison(cmd *cobra.Command, _ []string) error {
return err
}

req, err := archeraComparisonRequest(planID)
if err != nil {
return err
}

// Config and client stay local to this call; never store them in a
// package variable or struct.
client, err := insurance.NewClient(insurance.Config{APIKey: insurance.NewSecret(key), OrgID: orgID}, archeraHTTPClient)
if err != nil {
return err
}
comparison, err := client.Comparison(cmd.Context(), insurance.ComparisonRequest{PlanID: planID})
comparison, err := client.Comparison(cmd.Context(), req)
if err != nil {
return archeraSafeError(err, key)
}
Expand All @@ -96,6 +107,50 @@ func runArcheraComparison(cmd *cobra.Command, _ []string) error {
return renderArcheraTable(cmd.OutOrStdout(), dto)
}

func archeraComparisonRequest(planID string) (insurance.ComparisonRequest, error) {
req := insurance.ComparisonRequest{PlanID: planID}
filters := []struct {
values *[]string
flag string
raw []string
limit int
}{
{&req.LineItemIDs, "line-item-ids", archeraOpts.LineItemIDs, 200},
{&req.ContractTerms, "contract-terms", archeraOpts.ContractTerms, len(insurance.ContractTerms())},
}
for _, filter := range filters {
values, err := archeraFilterValues(filter.flag, filter.raw, filter.limit)
if err != nil {
return req, err
}
*filter.values = values
}
payments, err := archeraFilterValues("payment-options", archeraOpts.PaymentOptions, 3)
if err != nil {
return req, err
}
for _, payment := range payments {
req.PaymentOptions = append(req.PaymentOptions, insurance.PaymentOption(payment))
}
return req, nil
}

func archeraFilterValues(flag string, raw []string, limit int) ([]string, error) {
var values []string
for occurrence, entry := range raw {
for segment, value := range strings.Split(entry, ",") {
if strings.TrimSpace(value) == "" {
return nil, fmt.Errorf("--%s occurrence %d entry %d must not be empty", flag, occurrence+1, segment+1)
}
if len(values) >= limit {
return nil, fmt.Errorf("--%s accepts at most %d entries", flag, limit)
}
values = append(values, value)
}
}
return values, nil
}

// archeraSettings reads the key, org ID and plan ID, naming (never echoing)
// the first missing setting. The key is returned to RunE only.
func archeraSettings() (key, orgID, planID string, err error) {
Expand Down Expand Up @@ -131,6 +186,15 @@ const archeraWithheld = "[message withheld: contained the API key]"
func archeraSafeError(err error, key string) error {
key = strings.TrimSpace(key)
msg, suffix := err.Error(), ""
// The shared validator echoes rejected input; keep its validation but omit values.
switch {
case strings.HasPrefix(msg, "archera line item ID "):
msg = "--line-item-ids entries must be canonical UUIDs"
case strings.HasPrefix(msg, "unsupported contract term "):
msg = "--contract-terms entries must be supported Archera contract terms (exact case)"
case strings.HasPrefix(msg, "payment option: "):
msg = "--payment-options entries must be no_upfront, partial_upfront or all_upfront (exact case)"
}
var httpErr *insurance.HTTPError
isHTTP := errors.As(err, &httpErr)
if isHTTP && httpErr.RetryAfter > 0 {
Expand Down
115 changes: 115 additions & 0 deletions cmd/archera_comparison_filters_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package main

import (
"net/http"
"net/url"
"strings"
"testing"

"github.com/LeanerCloud/cloud-commitments-go/pkg/insurance"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

const archeraTestLine = "cccccccc-cccc-4ccc-8ccc-cccccccccccc"

func TestArcheraComparisonFiltersQuery(t *testing.T) {
terms := insurance.ContractTerms()
cases := []struct {
name string
args []string
want url.Values
}{
{"omitted", nil, url.Values{}},
{"line items", []string{"--line-item-ids", archeraTestLine}, url.Values{"line_item_ids": {archeraTestLine}}},
{"terms", []string{"--contract-terms", "one_year_gris,three_year"}, url.Values{"contract_terms": {"one_year_gris", "three_year"}}},
{"payments", []string{"--payment-options", "no_upfront", "--payment-options", "all_upfront"}, url.Values{"payment_options": {"no_upfront", "all_upfront"}}},
{"combined duplicates", []string{"--line-item-ids", archeraTestLine + "," + archeraTestOrg, "--line-item-ids", archeraTestLine, "--contract-terms", "one_year", "--payment-options", "partial_upfront"}, url.Values{"line_item_ids": {archeraTestLine, archeraTestOrg, archeraTestLine}, "contract_terms": {"one_year"}, "payment_options": {"partial_upfront"}}},
{"200 IDs", []string{"--line-item-ids", strings.TrimSuffix(strings.Repeat(archeraTestLine+",", 200), ",")}, url.Values{"line_item_ids": strings.Split(strings.TrimSuffix(strings.Repeat(archeraTestLine+",", 200), ","), ",")}},
{"all terms", []string{"--contract-terms", strings.Join(terms, ",")}, url.Values{"contract_terms": terms}},
{"three payments", []string{"--payment-options", "no_upfront,partial_upfront,all_upfront"}, url.Values{"payment_options": {"no_upfront", "partial_upfront", "all_upfront"}}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
rec := setupArchera(t, okHandler)
out, stderr, err := runArchera(t, fullEnv(), tc.args...)
require.NoError(t, err)
require.Len(t, rec.requests, 1)
req := rec.requests[0]
assert.Equal(t, http.MethodGet, req.Method)
assert.Equal(t, "/v1/org/"+archeraTestOrg+"/commitment-plans/"+archeraTestPlan+"/comparison", req.URL.Path)
assert.Equal(t, archeraTestKey, req.Header.Get("x-api-key"))
got := req.URL.Query()
require.Len(t, got, len(tc.want))
for key, values := range tc.want {
assert.ElementsMatch(t, values, got[key])
}
assert.NotContains(t, out+stderr, archeraTestKey)
})
}
}

func TestArcheraComparisonFiltersRejectBeforeHTTP(t *testing.T) {
for _, flag := range []string{"line-item-ids", "contract-terms", "payment-options"} {
for _, value := range []string{"", " ", ",", "one_year,", ",one_year", "one_year,,three_year", archeraTestKey, "ONE_YEAR"} {
t.Run(flag+"/"+value, func(t *testing.T) {
rec := setupArchera(t, func(http.ResponseWriter, *http.Request) { t.Error("unexpected HTTP request") })
out, stderr, err := runArchera(t, fullEnv(), "--"+flag+"="+value)
require.Error(t, err)
assert.NotContains(t, err.Error(), "unknown flag")
assert.Contains(t, err.Error(), "--"+flag)
assert.NotContains(t, out+stderr+err.Error(), archeraTestKey)
assert.Empty(t, rec.requests)
})
}
}
cases := []struct{ flag, value string }{
{"line-item-ids", strings.TrimSuffix(strings.Repeat(archeraTestLine+",", 201), ",")},
{"contract-terms", strings.Join(append(insurance.ContractTerms(), "one_year"), ",")},
{"payment-options", "no_upfront,no_upfront,no_upfront,no_upfront"},
{"line-item-ids", "cccccccccccc4ccc8ccccccccccccccc"},
{"line-item-ids", "{" + archeraTestLine + "}"},
{"line-item-ids", "urn:uuid:" + archeraTestLine},
{"contract-terms", " one_year"},
{"payment-options", "No_Upfront"},
}
for _, tc := range cases {
t.Run(tc.flag+"/"+tc.value[:min(12, len(tc.value))], func(t *testing.T) {
rec := setupArchera(t, func(http.ResponseWriter, *http.Request) { t.Error("unexpected HTTP request") })
_, _, err := runArchera(t, fullEnv(), "--"+tc.flag, tc.value)
require.ErrorContains(t, err, "--"+tc.flag)
assert.Empty(t, rec.requests)
})
}
for _, tc := range []struct{ flag, valid string }{{"line-item-ids", archeraTestLine}, {"contract-terms", "one_year"}, {"payment-options", "no_upfront"}} {
t.Run("repeated empty "+tc.flag, func(t *testing.T) {
rec := setupArchera(t, func(http.ResponseWriter, *http.Request) { t.Error("unexpected HTTP request") })
_, _, err := runArchera(t, fullEnv(), "--"+tc.flag, tc.valid, "--"+tc.flag+"=")
require.ErrorContains(t, err, "--"+tc.flag)
assert.Empty(t, rec.requests)
})
}
}

func TestArcheraComparisonFiltersHelpAndReset(t *testing.T) {
rec := setupArchera(t, okHandler)
out, _, err := runArchera(t, fullEnv(), "--help")
require.NoError(t, err)
for _, flag := range []string{"line-item-ids", "contract-terms", "payment-options"} {
assert.Contains(t, out, "--"+flag)
}
assert.Empty(t, rec.requests)
_, _, err = runArchera(t, fullEnv(), "--line-item-ids", archeraTestLine)
require.NoError(t, err)
_, _, err = runArchera(t, fullEnv())
require.NoError(t, err)
require.Len(t, rec.requests, 2)
assert.Empty(t, rec.requests[1].URL.RawQuery)
for _, flag := range []string{"line-item-ids", "contract-terms", "payment-options"} {
assert.False(t, archeraComparisonCmd.Flags().Changed(flag))
}
rootCmd.SetArgs([]string{"configure-gcp", "--help"})
require.NoError(t, rootCmd.Execute())
resetHelpFlags()
assert.Len(t, rec.requests, 2)
}
4 changes: 4 additions & 0 deletions cmd/archera_comparison_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,10 @@ func runArchera(t *testing.T, env map[string]string, args ...string) (stdout, st
}
resetHelpFlags()
archeraOpts.OrgID, archeraOpts.PlanID, archeraOpts.Format = "", "", "table"
archeraOpts.LineItemIDs, archeraOpts.ContractTerms, archeraOpts.PaymentOptions = nil, nil, nil
for _, name := range []string{"org-id", "plan-id", "format", "line-item-ids", "contract-terms", "payment-options"} {
archeraComparisonCmd.Flags().Lookup(name).Changed = false
}
var out, errb bytes.Buffer
rootCmd.SetOut(&out)
rootCmd.SetErr(&errb)
Expand Down
25 changes: 24 additions & 1 deletion docs/cli/archera-comparison.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,31 @@ export ARCHERA_ORG_ID=<org-uuid>
export ARCHERA_PLAN_ID=<plan-uuid>
ri-helper archera-comparison # table
ri-helper archera-comparison --format json # one JSON document on stdout
ri-helper archera-comparison --line-item-ids <line-item-uuid> \
--contract-terms one_year_gris,three_year \
--payment-options no_upfront --payment-options all_upfront
```

Optional filters narrow the vendor comparison rollups:

| Flag | Values | Maximum entries |
|------|--------|-----------------|
| `--line-item-ids` | canonical Archera line-item UUIDs | 200 |
| `--contract-terms` | exact, case-sensitive Archera contract-term enum values | 74 |
| `--payment-options` | `no_upfront`, `partial_upfront`, `all_upfront` | 3 |

Each filter accepts comma-separated entries, repeated flags, or both. Duplicate
entries are preserved and count toward the limit. Explicit empty occurrences,
empty comma segments and whitespace-only entries are errors before any request.
Entries are not trimmed or case-normalized. Invalid UUIDs and enum values also
fail before any request; errors do not echo rejected values.

Omitting a filter sends no corresponding query parameter. The documented vendor
defaults are all selected line items, every distinct candidate term, and
`no_upfront` only, respectively. The CLI does not replace those defaults.
Line-item IDs come from Archera, not platform recommendation IDs. These filters do not prove
an exact recommendation association or economic equivalence.

Errors go to stderr and the exit status is 1 for every failure. A vendor error
shows the sanitized HTTP status and message, plus `Retry-After` when given
(capped at 24 hours). The command does not retry.
Expand All @@ -41,7 +64,7 @@ shows the sanitized HTTP status and message, plus `Retry-After` when given
- Unknown values print `unknown` in the table and `null` in JSON, never `0`.
- Money is printed as an exact decimal. `discount_rate` is shown as sent
(0-1 basis).
- The comparison is a hypothetical rollup for the whole plan. It is not a
- The comparison is a hypothetical rollup for the plan, narrowed by any filters. It is not a
bindable quote, not a guarantee, and not mapped onto recommendation rows or
CSV output.
- Product support is `supported` only when Archera's documentation lists the
Expand Down
Loading