From f0ca94041bb405966923a28136b427027b6252b9 Mon Sep 17 00:00:00 2001 From: Steve Ramage Date: Fri, 24 Oct 2025 10:38:43 -0700 Subject: [PATCH] feat: Add support for manual orders (Resolves #580) feat: add support for --page-length to delete-all feat: allow runbooks to have an enum option. feat: add support for cart-custom-discounts fix: catch error when regex params use [n] unescaped in resources.yaml fix: update sso urls feat: add self sign up runbook feat: add currencies runbook feat: add display-cart runbook --- cmd/delete-all.go | 26 ++- cmd/reset-store.go | 2 +- cmd/runbooks.go | 13 ++ external/apihelper/get_all_ids.go | 15 +- external/completion/completion.go | 2 + external/completion/completion_test.go | 29 +++ external/resources/resources.go | 2 + external/resources/resources_schema.json | 11 +- external/resources/uritemplates.go | 5 +- external/resources/uritemplates_test.go | 3 +- external/resources/yaml/carts-and-orders.yaml | 205 ++++++++++++++++++ .../resources/yaml/resources_yaml_test.go | 40 +++- external/resources/yaml/single-sign-on.yaml | 72 +++--- external/rest/get.go | 4 + external/runbooks/account-management.epcc.yml | 9 + external/runbooks/currencies.epcc.yml | 32 +++ external/runbooks/manual-orders.epcc.yml | 202 +++++++++++++++++ external/runbooks/rule-promotions.epcc.yml | 16 ++ external/runbooks/run-all-runbooks.sh | 48 ++++ external/runbooks/runbook_rendering.go | 14 ++ external/templates/funcs.go | 77 ++++++- 21 files changed, 768 insertions(+), 59 deletions(-) create mode 100644 external/runbooks/currencies.epcc.yml create mode 100644 external/runbooks/manual-orders.epcc.yml diff --git a/cmd/delete-all.go b/cmd/delete-all.go index fc58f615..9548de0e 100644 --- a/cmd/delete-all.go +++ b/cmd/delete-all.go @@ -24,6 +24,12 @@ import ( func NewDeleteAllCommand(parentCmd *cobra.Command) func() { + var pageLength uint16 = 25 + + resetFunc := func() { + pageLength = 25 + } + var deleteAll = &cobra.Command{ Use: "delete-all", Short: "Deletes all of a resource", @@ -37,6 +43,8 @@ func NewDeleteAllCommand(parentCmd *cobra.Command) func() { }, } + deleteAll.PersistentFlags().Uint16VarP(&pageLength, "page-length", "", pageLength, "page length to use when deleting") + e := config.GetEnv() hiddenResources := map[string]struct{}{} @@ -71,16 +79,16 @@ func NewDeleteAllCommand(parentCmd *cobra.Command) func() { Short: GetDeleteAllShort(resource), Hidden: false, RunE: func(cmd *cobra.Command, args []string) error { - return deleteAllInternal(clictx.Ctx, append([]string{resourceName}, args...)) + return deleteAllInternal(clictx.Ctx, pageLength, append([]string{resourceName}, args...)) }, } deleteAll.AddCommand(deleteAllResourceCmd) } parentCmd.AddCommand(deleteAll) - return func() {} + return resetFunc } -func deleteAllInternal(ctx context.Context, args []string) error { +func deleteAllInternal(ctx context.Context, pageLength uint16, args []string) error { // Find Resource resource, ok := resources.GetResourceByName(args[0]) if !ok { @@ -95,7 +103,7 @@ func deleteAllInternal(ctx context.Context, args []string) error { return fmt.Errorf("resource %s doesn't support DELETE", args[0]) } - allParentEntityIds, err := getParentIds(ctx, resource) + allParentEntityIds, err := getParentIds(ctx, pageLength, resource) if err != nil { return fmt.Errorf("could not retrieve parent ids for for resource %s, error: %w", resource.PluralName, err) @@ -117,7 +125,11 @@ func deleteAllInternal(ctx context.Context, args []string) error { } params := url.Values{} - params.Add("page[limit]", "25") + params.Add("page[limit]", fmt.Sprintf("%d", pageLength)) + + for k, v := range resource.GetCollectionInfo.DefaultQueryParams { + params.Add(k, v) + } resp, err := httpclient.DoRequest(ctx, "GET", resourceURL, params.Encode(), nil) @@ -175,7 +187,7 @@ func deleteAllInternal(ctx context.Context, args []string) error { return aliases.ClearAllAliasesForJsonApiType(resource.JsonApiType) } -func getParentIds(ctx context.Context, resource resources.Resource) ([][]id.IdableAttributes, error) { +func getParentIds(ctx context.Context, pageLength uint16, resource resources.Resource) ([][]id.IdableAttributes, error) { myEntityIds := make([][]id.IdableAttributes, 0) if resource.GetCollectionInfo == nil { @@ -200,7 +212,7 @@ func getParentIds(ctx context.Context, resource resources.Resource) ([][]id.Idab return myEntityIds, fmt.Errorf("could not find parent resource %s", immediateParentType) } - return apihelper.GetAllIds(ctx, &parentResource) + return apihelper.GetAllIds(ctx, pageLength, &parentResource) } } diff --git a/cmd/reset-store.go b/cmd/reset-store.go index 6d9a9fab..7bd349a0 100644 --- a/cmd/reset-store.go +++ b/cmd/reset-store.go @@ -310,7 +310,7 @@ func deleteAllResourceData(resourceNames []string) (error, []string) { if myDepth == depth { log.Infof("Processing resource %s", resourceName) - err := deleteAllInternal(clictx.Ctx, []string{resourceName}) + err := deleteAllInternal(clictx.Ctx, 25, []string{resourceName}) if err != nil { errors = append(errors, fmt.Errorf("error while deleting %s: %w", resourceName, err).Error()) diff --git a/cmd/runbooks.go b/cmd/runbooks.go index 76812adc..86406959 100644 --- a/cmd/runbooks.go +++ b/cmd/runbooks.go @@ -466,6 +466,15 @@ func processRunbookVariablesOnCommand(runbookActionRunActionCommand *cobra.Comma description = variable.Description.Short } + // Add ENUM options to description + if strings.HasPrefix(variable.Type, "ENUM:") { + enumValues := strings.Split(variable.Type[5:], ",") + if description != "" { + description += ". " + } + description += "Options: [" + strings.Join(enumValues, ", ") + "]" + } + runbookActionRunActionCommand.Flags().StringVar(runbookStringArguments[key], key, templates.Render(variable.Default), description) } @@ -479,6 +488,10 @@ func processRunbookVariablesOnCommand(runbookActionRunActionCommand *cobra.Comma }) } + } else if strings.HasPrefix(variable.Type, "ENUM:") { + // Extract enum values from "ENUM:val1,val2,val3" + enumValues := strings.Split(variable.Type[5:], ",") + return enumValues, cobra.ShellCompDirectiveNoFileComp } return []string{}, cobra.ShellCompDirectiveNoFileComp diff --git a/external/apihelper/get_all_ids.go b/external/apihelper/get_all_ids.go index 3fb4cb37..15f69ff7 100644 --- a/external/apihelper/get_all_ids.go +++ b/external/apihelper/get_all_ids.go @@ -3,15 +3,16 @@ package apihelper import ( "context" "fmt" + "net/url" + "reflect" + "github.com/elasticpath/epcc-cli/external/httpclient" "github.com/elasticpath/epcc-cli/external/id" "github.com/elasticpath/epcc-cli/external/resources" log "github.com/sirupsen/logrus" - "net/url" - "reflect" ) -func GetAllIds(ctx context.Context, resource *resources.Resource) ([][]id.IdableAttributes, error) { +func GetAllIds(ctx context.Context, pageLength uint16, resource *resources.Resource) ([][]id.IdableAttributes, error) { // TODO make this a channel based instead of array based // This must be an unbuffered channel since the receiver won't get the channel until after we have sent in some cases. //myEntityIds := make(chan<- []string, 1024) @@ -49,7 +50,7 @@ func GetAllIds(ctx context.Context, resource *resources.Resource) ([][]id.Idable parentResource = &myParentResource } - myParentEntityIds, err := GetAllIds(ctx, parentResource) + myParentEntityIds, err := GetAllIds(ctx, pageLength, parentResource) if err != nil { return myEntityIds, err } @@ -66,9 +67,13 @@ func GetAllIds(ctx context.Context, resource *resources.Resource) ([][]id.Idable lastPageIds := make([]id.IdableAttributes, 125) for i := 0; i < 10000; i += 25 { params := url.Values{} - params.Add("page[limit]", "25") + params.Add("page[limit]", fmt.Sprintf("%d", pageLength)) params.Add("page[offset]", fmt.Sprintf("%d", i)) + for k, v := range resource.GetCollectionInfo.DefaultQueryParams { + params.Add(k, v) + } + resp, err := httpclient.DoRequest(ctx, "GET", resourceURL, params.Encode(), nil) if resp != nil && resp.Body != nil { diff --git a/external/completion/completion.go b/external/completion/completion.go index d1e8a085..507511cd 100644 --- a/external/completion/completion.go +++ b/external/completion/completion.go @@ -187,6 +187,8 @@ func Complete(c Request) ([]string, cobra.ShellCompDirective) { if regexOptions, err := rt.GetCompletionOptions(); err == nil { autoCompleteAttributes = append(autoCompleteAttributes, regexOptions...) + } else { + log.Debugf("Could not complete options %v", err) } for _, k := range autoCompleteAttributes { diff --git a/external/completion/completion_test.go b/external/completion/completion_test.go index 59866423..13aedba6 100644 --- a/external/completion/completion_test.go +++ b/external/completion/completion_test.go @@ -821,6 +821,35 @@ func TestCompleteAttributeKeyWithWhenAndSatisfiedExistingValuesReturnsSatisfiedC require.Len(t, completions, 9) } +// This test might be redundant but regex was broken for this resource at a time +func TestCompleteAttributeKeyWithEmptyExistingValuesReturnsAllIncludingRegex(t *testing.T) { + // Fixture Setup + toComplete := "" + manualOrder := resources.MustGetResourceByName("manual-order") + request := Request{ + Type: CompleteAttributeKey, + Verb: Create, + ToComplete: toComplete, + Resource: manualOrder, + Attributes: map[string]string{ + "meta.display_price.with_tax.currency": "USD", + "meta.display_price.balance_owing.formatted": "$10.00", + }, + } + + // Exercise SUT + completions, compDir := Complete(request) + + // Verify Results + require.Equal(t, compDir, cobra.ShellCompDirectiveNoFileComp) + + require.Contains(t, completions, "meta.display_price.with_tax.amount") + require.Contains(t, completions, "meta.display_price.balance_owing.currency") + require.NotContains(t, completions, "meta.display_price.with_tax.currency") + require.NotContains(t, completions, "meta.display_price.balance_owing.formatted") + require.Contains(t, completions, "meta.display_price.paid") +} + func TestCompleteAttributeKeyWithWhenSkippingWhen(t *testing.T) { // Fixture Setup toComplete := "" diff --git a/external/resources/resources.go b/external/resources/resources.go index f07aac41..46e18dfa 100644 --- a/external/resources/resources.go +++ b/external/resources/resources.go @@ -116,6 +116,8 @@ type CrudEntityInfo struct { // Only valid on create, if set we report that the type created by this is different. Creates string `yaml:"creates"` + + DefaultQueryParams map[string]string `yaml:"default-query-params,omitempty"` } type CrudEntityAttribute struct { diff --git a/external/resources/resources_schema.json b/external/resources/resources_schema.json index 3fb9b5f4..219d00c4 100644 --- a/external/resources/resources_schema.json +++ b/external/resources/resources_schema.json @@ -66,7 +66,16 @@ "additionalProperties": false } }, - "openapi-operation-id": { "type": "string" } + "openapi-operation-id": { "type": "string" }, + "default-query-params": { + "type": "object", + "additionalProperties": true, + "patternProperties": { + "(.*?)": { + "type": "string" + } + } + } }, "required": [ "url", "docs"] }, diff --git a/external/resources/uritemplates.go b/external/resources/uritemplates.go index fa766e52..1e06a32e 100644 --- a/external/resources/uritemplates.go +++ b/external/resources/uritemplates.go @@ -2,12 +2,13 @@ package resources import ( "fmt" + "net/url" + "strings" + "github.com/elasticpath/epcc-cli/external/aliases" "github.com/elasticpath/epcc-cli/external/id" log "github.com/sirupsen/logrus" "github.com/yosida95/uritemplate/v3" - "net/url" - "strings" ) func GenerateUrlViaIdableAttributes(urlInfo *CrudEntityInfo, args []id.IdableAttributes) (string, error) { diff --git a/external/resources/uritemplates_test.go b/external/resources/uritemplates_test.go index e6ad2936..87546482 100644 --- a/external/resources/uritemplates_test.go +++ b/external/resources/uritemplates_test.go @@ -1,8 +1,9 @@ package resources import ( - "github.com/elasticpath/epcc-cli/external/aliases" "testing" + + "github.com/elasticpath/epcc-cli/external/aliases" ) func TestGetNumberOfVariablesReturnsErrorOnTemplate(t *testing.T) { diff --git a/external/resources/yaml/carts-and-orders.yaml b/external/resources/yaml/carts-and-orders.yaml index e4cbdb38..8f644f8b 100644 --- a/external/resources/yaml/carts-and-orders.yaml +++ b/external/resources/yaml/carts-and-orders.yaml @@ -71,6 +71,41 @@ cart-items: type: INT id: type: RESOURCE_ID:pcm-products +cart-custom-discounts: + singular-name: "cart-custom-discount" + json-api-type: "custom_discount" + json-api-format: "legacy" + no-wrapping: true + docs: "https://developer.elasticpath.com/docs/api/carts/bulk-add-custom-discounts-to-cart" + create-entity: + docs: "https://developer.elasticpath.com/docs/api/carts/bulk-add-custom-discounts-to-cart" + url: "/v2/carts/{carts}/custom-discounts" + openapi-operation-id: bulkAddCustomDiscountsToCart + content-type: application/json + delete-entity: + docs: "https://developer.elasticpath.com/docs/api/carts/bulk-delete-custom-discounts-from-cart" + url: "/v2/carts/{carts}/custom-discounts" + openapi-operation-id: bulkDeleteCustomDiscountsFromCart + content-type: application/json + attributes: + data[n].amount: + type: INT + data[n].amount.amount: + type: INT + data[n].amount.currency: + type: CURRENCY + data[n].amount.formatted: + type: STRING + data[n].external_id: + type: STRING + data[n].discount_code: + type: STRING + data[n].type: + type: CONST:custom_discount + data[n].description: + type: STRING + + cart-product-items: singular-name: "cart-product-item" json-api-type: "cart_item" @@ -387,3 +422,173 @@ order-transaction-refund: docs: "https://elasticpath.dev/docs/carts-orders/refund-a-transaction" url: "/v2/orders/{orders}/transactions/{order_transactions}/refund" openapi-operation-id: refundATransaction + +manual-orders: + singular-name: "manual-order" + json-api-type: order + json-api-format: legacy + docs: "https://elasticpath.dev/docs/api/carts/orders" + create-entity: + docs: "https://elasticpath.dev/docs/api/carts/orders" + url: "/v2/orders" + get-collection: + docs: "https://elasticpath.dev/docs/api/carts/orders" + url: "/v2/orders" + default-query-params: + filter: "eq(manual,true)" + openapi-operation-id: getCustomerOrders + query: + - name: include + type: STRING + - name: page[limit] + type: INT + - name: page[offset] + type: INT + - name: filter + type: STRING + delete-entity: + docs: "https://elasticpath.dev/docs/api/carts/orders" + url: "/v2/orders/{orders}" + + + attributes: + id: + type: STRING + usage: "Optional custom order ID. If not provided, a UUID will be generated." + status: + type: ENUM:incomplete,processing,cancelled,complete + usage: "The status of the order." + payment: + type: ENUM:paid,unpaid,refunded,partially_authorized,partially_paid + usage: "The payment status of the order." + shipping: + type: ENUM:fulfilled,unfulfilled + usage: "The shipping status of the order." + anonymized: + type: BOOL + usage: "Whether the order should be anonymized." + account.id: + type: RESOURCE_ID:account + usage: "The unique identifier of the account." + account.member_id: + type: RESOURCE_ID:account-member + usage: "The unique identifier of the account member." + contact.name: + type: STRING + usage: "Contact name for the order." + contact.email: + type: STRING + usage: "Contact email for the order." + customer.id: + type: RESOURCE_ID:customer + usage: "The unique identifier of the customer." + autofill: FUNC:UUID + customer.email: + type: STRING + usage: "Customer email address." + autofill: FUNC:Email + customer.name: + type: STRING + usage: "Customer name." + autofill: FUNC:Name + shipping_address.first_name: + type: STRING + autofill: FUNC:FirstName + shipping_address.last_name: + type: STRING + autofill: FUNC:LastName + shipping_address.company_name: + type: STRING + autofill: FUNC:Company + shipping_address.line_1: + type: STRING + autofill: FUNC:Street + shipping_address.line_2: + type: STRING + shipping_address.city: + type: STRING + autofill: FUNC:City + shipping_address.county: + type: STRING + shipping_address.region: + type: STRING + autofill: FUNC:State + shipping_address.postcode: + type: STRING + autofill: FUNC:Zip + shipping_address.country: + type: STRING + autofill: FUNC:Country + billing_address.first_name: + type: STRING + autofill: FUNC:FirstName + billing_address.last_name: + type: STRING + autofill: FUNC:LastName + billing_address.company_name: + type: STRING + autofill: FUNC:Company + billing_address.line_1: + type: STRING + autofill: FUNC:Street + billing_address.line_2: + type: STRING + billing_address.city: + type: STRING + autofill: FUNC:City + billing_address.county: + type: STRING + billing_address.region: + type: STRING + autofill: FUNC:State + billing_address.postcode: + type: STRING + autofill: FUNC:Zip + billing_address.country: + type: STRING + autofill: FUNC:Country + ^meta\.display_price\.(with_tax|without_tax|tax|discount|balance_owing|paid|authorized|without_discount|shipping|shipping_discount)\.amount$: + type: INT + usage: "The amount of the order specified in currency subunits" + ^meta\.display_price\.(with_tax|without_tax|tax|discount|balance_owing|paid|authorized|without_discount|shipping|shipping_discount)\.currency$: + type: CURRENCY + usage: "The currency" + ^meta\.display_price\.(with_tax|without_tax|tax|discount|balance_owing|paid|authorized|without_discount|shipping|shipping_discount)\.formatted$: + type: STRING + usage: "The amount of the order specified as a formatted string" + included.items[n].id: + type: STRING + usage: "The unique identifier of the item." + included.items[n].type: + type: CONST:order_item + included.items[n].quantity: + type: INT + included.items[n].location: + type: STRING + included.items[n].product_id: + type: RESOURCE_ID:pcm-product + included.items[n].subscription_offering_id: + type: RESOURCE_ID:subscription-offerings + included.items[n].name: + type: STRING + included.items[n].sku: + type: STRING + ^included\.items\[n\]\.(unit_price|value)\.amount$: + type: INT + usage: "An amount in currency subunits if applicable (e.g., $100.00 would be 10000)" + ^included\.items\[n\]\.(unit_price|value)\.currency$: + type: CURRENCY + ^included\.items\[n\]\.(unit_price|value)\.include_tax$: + type: BOOL + ^included\.items\[n\]\.meta\.display_price\.(with_tax|without_tax|tax|discount_without_discount)\.(unit|value)\.amount$: + type: INT + usage: "An amount in currency subunits if applicable (e.g., $100.00 would be 10000)" + ^included\.items\[n\]\.meta\.display_price\.(with_tax|without_tax|tax|discount_without_discount)\.(unit|value)\.currency$: + type: CURRENCY + ^included\.items\[n\]\.meta\.display_price\.(with_tax|without_tax|tax|discount_without_discount)\.(unit|value)\.formatted$: + usage: "A formatted amount e.g., $100.00" + type: STRING + included.items[n].meta.timestamps.created_at: + type: STRING + included.items[n].meta.timestamps.updated_at: + type: STRING diff --git a/external/resources/yaml/resources_yaml_test.go b/external/resources/yaml/resources_yaml_test.go index 3e64b4f5..9cfc2b63 100644 --- a/external/resources/yaml/resources_yaml_test.go +++ b/external/resources/yaml/resources_yaml_test.go @@ -18,6 +18,7 @@ import ( "github.com/expr-lang/expr" "github.com/expr-lang/expr/ast" "github.com/expr-lang/expr/parser" + "github.com/quasilyte/regex/syntax" "github.com/santhosh-tekuri/jsonschema/v4" log "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" @@ -34,7 +35,7 @@ func TestExpectedNumberOfResources(t *testing.T) { resourceCount := len(resources.GetPluralResources()) // Verification - require.Equal(t, resourceCount, 138) + require.Equal(t, resourceCount, 140) } func TestCreatedByTemplatesAllReferenceValidResource(t *testing.T) { @@ -244,6 +245,15 @@ func validateAttributeInfo(info *resources.CrudEntityAttribute) string { errors += fmt.Sprintf("\t attribute `%s` is a regex, but the completion tree doesn't support it: %v", info.Key, err) } + p := syntax.NewParser(&syntax.ParserOptions{}) + parse, err := p.Parse(info.Key) + + err = validateRegexTreeContainsNoSingleCharClass(parse) + + if err != nil { + errors += fmt.Sprintf("\t attribute `%s` is a regex, but it contains a single char class: %v", info.Key, err) + } + } } if match { @@ -264,6 +274,34 @@ func validateAttributeInfo(info *resources.CrudEntityAttribute) string { return errors } + +func validateRegexTreeContainsNoSingleCharClass(tree *syntax.Regexp) error { + + var checkChildren func(e *syntax.Expr) error + checkChildren = func(e *syntax.Expr) error { + + if e.Op == syntax.OpCharClass { + if len(e.Args) == 1 { + return fmt.Errorf("single char class found: %v", e.Value) + } + } + + for _, child := range e.Args { + + err := checkChildren(&child) + + if err != nil { + return err + } + + } + + return nil + } + + return checkChildren(&tree.Expr) +} + func validateCrudEntityInfo(info resources.CrudEntityInfo) string { errors := "" diff --git a/external/resources/yaml/single-sign-on.yaml b/external/resources/yaml/single-sign-on.yaml index 79130ce0..9625bfc0 100644 --- a/external/resources/yaml/single-sign-on.yaml +++ b/external/resources/yaml/single-sign-on.yaml @@ -4,9 +4,9 @@ authentication-realms: alternate-json-type-for-aliases: - authentication_realm json-api-format: "legacy" - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/authentication-realm-api/authentication-realm-api-overview" + docs: "https://elasticpath.dev/docs/api/single-sign-on/authentication-realms" get-collection: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/authentication-realm-api/get-all-authentication-realms" + docs: "https://elasticpath.dev/docs/api/single-sign-on/get-v-2-authentication-realms" url: "/v2/authentication-realms" openapi-operation-id: "get-v2-authentication-realms" query: @@ -15,11 +15,11 @@ authentication-realms: - name: page[offset] type: INT get-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/authentication-realm-api/get-an-authentication-realm" + docs: "https://elasticpath.dev/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id" url: "/v2/authentication-realms/{authentication_realms}" openapi-operation-id: "get-v2-authentication-realms-realmId" update-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/authentication-realm-api/update-an-authentication-realm" + docs: "https://elasticpath.dev/docs/api/single-sign-on/put-v-2-authentication-realms-realm-id" url: "/v2/authentication-realms/{authentication_realms}" openapi-operation-id: "put-v2-authentication-realms-realmId" attributes: @@ -41,9 +41,9 @@ oidc-profiles: singular-name: "oidc-profile" json-api-type: "oidc-profile" json-api-format: "legacy" - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/openid-connect-profiles-api/openid-connect-profiles-api-overview" + docs: "https://developer.elasticpath.com/docs/api/single-sign-on/oidc-profiles" get-collection: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/openid-connect-profiles-api/get-all-oidc-profiles" + docs: "https://developer.elasticpath.com/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id-oidc-profiles" url: "/v2/authentication-realms/{authentication_realms}/oidc-profiles" openapi-operation-id: "get-v2-authentication-realms-realmId-oidc-profiles" query: @@ -52,19 +52,19 @@ oidc-profiles: - name: page[offset] type: INT get-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/openid-connect-profiles-api/get-an-oidc-profile" + docs: "https://developer.elasticpath.com/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id-oidc-profiles-profile-id" url: "/v2/authentication-realms/{authentication_realms}/oidc-profiles/{oidc_profiles}" openapi-operation-id: "get-v2-authentication-realms-realmId-oidc-profiles-profileId" update-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/openid-connect-profiles-api/update-an-oidc-profile" + docs: "https://developer.elasticpath.com/docs/api/single-sign-on/put-v-2-authentication-realms-realm-id-oidc-profiles-profile-id" url: "/v2/authentication-realms/{authentication_realms}/oidc-profiles/{oidc_profiles}" openapi-operation-id: "put-v2-authentication-realms-realmId-oidc-profiles-profileId" delete-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/openid-connect-profiles-api/delete-an-oidc-profile" + docs: "https://developer.elasticpath.com/docs/api/single-sign-on/delete-v-2-authentication-realms-realm-id-oidc-profiles-profile-id" url: "/v2/authentication-realms/{authentication_realms}/oidc-profiles/{oidc_profiles}" openapi-operation-id: "delete-v2-authentication-realms-realmId-oidc-profiles-profileId" create-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/openid-connect-profiles-api/create-an-oidc-profile" + docs: "https://developer.elasticpath.com/docs/api/single-sign-on/post-v-2-authentication-realms-realm-id-oidc-profiles" url: "/v2/authentication-realms/{authentication_realms}/oidc-profiles" openapi-operation-id: "post-v2-authentication-realms-realmId-oidc-profiles" content-type: application/json @@ -82,9 +82,9 @@ password-profiles: singular-name: "password-profile" json-api-type: "password_profile" json-api-format: "legacy" - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/overview" + docs: "https://elasticpath.dev/docs/api/single-sign-on/password-profiles" get-collection: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/get-all-password-profiles" + docs: "https://elasticpath.dev/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id-password-profiles" url: "/v2/authentication-realms/{authentication_realms}/password-profiles" openapi-operation-id: "get-v2-authentication-realms-realmId-password-profiles" query: @@ -93,19 +93,19 @@ password-profiles: - name: page[offset] type: INT get-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/get-a-password-profile" + docs: "https://elasticpath.dev/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id-password-profiles-profile-id" url: "/v2/authentication-realms/{authentication_realms}/password-profiles/{password_profiles}" openapi-operation-id: "get-v2-authentication-realms-realmId-password-profiles-profileId" update-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/update-a-password-profile" + docs: "https://elasticpath.dev/docs/api/single-sign-on/put-v-2-authentication-realms-realm-id-password-profiles-profile-id" url: "/v2/authentication-realms/{authentication_realms}/password-profiles/{password_profiles}" openapi-operation-id: "put-v2-authentication-realms-realmId-password-profiles-profileId" delete-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/delete-a-password-profile" + docs: "https://elasticpath.dev/docs/api/single-sign-on/delete-v-2-authentication-realms-realm-id-password-profiles-profile-id" url: "/v2/authentication-realms/{authentication_realms}/password-profiles/{password_profiles}" openapi-operation-id: "delete-v2-authentication-realms-realmId-password-profiles-profileId" create-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/overview" + docs: "https://elasticpath.dev/docs/api/single-sign-on/password-profiles" url: "/v2/authentication-realms/{authentication_realms}/password-profiles" openapi-operation-id: "post-v2-authentication-realms-realmId-password-profiles" content-type: application/json @@ -120,9 +120,9 @@ one-time-password-token-requests: singular-name: "one-time-password-token-request" json-api-type: "one_time_password_token_request" json-api-format: "legacy" - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/create-one-time-password-token-request" + docs: "https://elasticpath.dev/docs/api/single-sign-on/post-v-2-authentication-realms-realm-id-password-profiles-profile-id-one-time-password-token-request" create-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/create-one-time-password-token-request" + docs: "https://elasticpath.dev/docs/api/single-sign-on/post-v-2-authentication-realms-realm-id-password-profiles-profile-id-one-time-password-token-request" url: "/v2/authentication-realms/{authentication_realms}/password-profiles/{password_profiles}/one-time-password-token-request" openapi-operation-id: "post-v2-authentication-realms-realmId-password-profiles-profileId-one-time-password-token-request" content-type: application/json @@ -136,9 +136,9 @@ user-authentication-infos: singular-name: "user-authentication-info" json-api-type: "user_authentication_info" json-api-format: "legacy" - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/user-authentication-info-api/overview" + docs: "https://elasticpath.dev/docs/api/single-sign-on/user-authentication-infos" get-collection: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/user-authentication-info-api/get-all-user-authentication-info" + docs: "https://elasticpath.dev/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id-user-authentication-info" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info" openapi-operation-id: "get-v2-authentication-realms-realmId-user-authentication-info" query: @@ -151,19 +151,19 @@ user-authentication-infos: - name: sort type: ENUM:created_at,-created_at,id,-id,updated_at,-updated_at get-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/user-authentication-info-api/get-a-user-authentication-info" + docs: "https://elasticpath.dev/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id-user-authentication-info-user-auth-info-id" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}" openapi-operation-id: "get-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId" update-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/user-authentication-info-api/update-a-user-authentication-info" + docs: "https://elasticpath.dev/docs/api/single-sign-on/put-v-2-authentication-realms-realm-id-user-authentication-info-user-auth-info-id" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}" openapi-operation-id: "put-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId" delete-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/user-authentication-info-api/delete-a-user-authentication-info" + docs: "https://elasticpath.dev/docs/api/single-sign-on/delete-v-2-authentication-realms-realm-id-user-authentication-info-user-auth-info-id" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}" openapi-operation-id: "delete-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId" create-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/user-authentication-info-api/create-a-user-authentication-info" + docs: "https://elasticpath.dev/docs/api/single-sign-on/post-v-2-authentication-realms-realm-id-user-authentication-info" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info" openapi-operation-id: "post-v2-authentication-realms-realmId-user-authentication-info" content-type: application/json @@ -178,9 +178,9 @@ user-authentication-oidc-profile-infos: singular-name: "user-authentication-oidc-profile-info" json-api-type: "user_authentication_oidc_profile_info" json-api-format: "legacy" - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/openid" + docs: "https://elasticpath.dev/docs/api/single-sign-on" get-collection: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/get-all-password-profiles" + docs: "https://elasticpath.dev/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id-password-profiles" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}/user-authentication-oidc-profile-info" openapi-operation-id: "get-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId-user-authentication-oidc-profile-info" query: @@ -189,19 +189,19 @@ user-authentication-oidc-profile-infos: - name: page[offset] type: INT get-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/get-a-password-profile" + docs: "https://elasticpath.dev/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id-password-profiles-profile-id" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}/user-authentication-oidc-profile-info/{user_authentication_oidc_profile_infos}" openapi-operation-id: "get-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId-user-authentication-oidc-profile-info-oidcInfoId" update-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/overview" + docs: "https://elasticpath.dev/docs/api/single-sign-on/password-profiles" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}/user-authentication-oidc-profile-info/{user_authentication_oidc_profile_infos}" openapi-operation-id: "put-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId-user-authentication-oidc-profile-info-oidcInfoId" delete-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/delete-a-password-profile" + docs: "https://elasticpath.dev/docs/api/single-sign-on/delete-v-2-authentication-realms-realm-id-password-profiles-profile-id" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}/user-authentication-oidc-profile-info/{user_authentication_oidc_profile_infos}" openapi-operation-id: "delete-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId-user-authentication-oidc-profile-info-oidcInfoId" create-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/overview" + docs: "https://elasticpath.dev/docs/api/single-sign-on/password-profiles" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}/user-authentication-oidc-profile-info" openapi-operation-id: "post-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId-user-authentication-oidc-profile-info" content-type: application/json @@ -216,9 +216,9 @@ user-authentication-password-profile-infos: singular-name: "user-authentication-password-profile-info" json-api-type: "user_authentication_password_profile_info" json-api-format: "legacy" - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/overview" + docs: "https://elasticpath.dev/docs/api/single-sign-on/password-profiles" get-collection: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/get-all-password-profiles" + docs: "https://elasticpath.dev/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id-password-profiles" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}/user-authentication-password-profile-info" openapi-operation-id: "get-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId-user-authentication-password-profile-info" query: @@ -227,19 +227,19 @@ user-authentication-password-profile-infos: - name: page[offset] type: INT get-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/get-all-password-profiles" + docs: "https://elasticpath.dev/docs/api/single-sign-on/get-v-2-authentication-realms-realm-id-password-profiles" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}/user-authentication-password-profile-info/{user_authentication_password_profile_infos}" openapi-operation-id: "get-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId-user-authentication-password-profile-info-passwordProfileInfoId" update-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/overview" + docs: "https://elasticpath.dev/docs/api/single-sign-on/password-profiles" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}/user-authentication-password-profile-info/{user_authentication_password_profile_infos}" openapi-operation-id: "put-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId-user-authentication-password-profile-info-passwordProfileInfoId" delete-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/overview" + docs: "https://elasticpath.dev/docs/api/single-sign-on/password-profiles" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}/user-authentication-password-profile-info/{user_authentication_password_profile_infos}" openapi-operation-id: "delete-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId-user-authentication-password-profile-info-passwordProfileInfoId" create-entity: - docs: "https://elasticpath.dev/docs/authentication/single-sign-on/password-profiles-api/overview" + docs: "https://elasticpath.dev/docs/api/single-sign-on/password-profiles" url: "/v2/authentication-realms/{authentication_realms}/user-authentication-info/{user_authentication_infos}/user-authentication-password-profile-info" openapi-operation-id: "post-v2-authentication-realms-realmId-user-authentication-info-userAuthInfoId-user-authentication-password-profile-info" content-type: application/json diff --git a/external/rest/get.go b/external/rest/get.go index 75801a66..804497a6 100644 --- a/external/rest/get.go +++ b/external/rest/get.go @@ -116,6 +116,10 @@ func GetResource(ctx context.Context, overrides *httpclient.HttpParameterOverrid } } + for k, v := range resourceUrlInfo.DefaultQueryParams { + params.Add(k, v) + } + for i := idCount + 1; i+1 < len(args); i = i + 2 { params.Add(args[i], args[i+1]) } diff --git a/external/runbooks/account-management.epcc.yml b/external/runbooks/account-management.epcc.yml index bfbc2dfb..820f8b6b 100644 --- a/external/runbooks/account-management.epcc.yml +++ b/external/runbooks/account-management.epcc.yml @@ -10,6 +10,15 @@ actions: # Initialize alias for Authentication Realm - epcc get account-authentication-settings - epcc create password-profile related_authentication_realm_for_account_authentication_settings_last_read=entity name "Username and Password Authentication" + enable-self-signup-and-management: + description: + short: "Enable password authentication" + commands: + # Initialize alias for Authentication Realm + - epcc get account-authentication-settings + - | + epcc create password-profile related_authentication_realm_for_account_authentication_settings_last_read=entity name "Username and Password Authentication" + epcc update account-authentication-setting enable_self_signup true auto_create_account_for_account_members true account_member_self_management "update_only" create-deep-hierarchy: description: short: "Create a hierarchy" diff --git a/external/runbooks/currencies.epcc.yml b/external/runbooks/currencies.epcc.yml new file mode 100644 index 00000000..236e929f --- /dev/null +++ b/external/runbooks/currencies.epcc.yml @@ -0,0 +1,32 @@ +name: "currencies" +description: + short: "Manage currencies in the store" + long: "A runbook to add common currencies and reset them safely" +actions: + add-currencies: + description: + short: "Add common currencies (EUR, GBP, CAD, CHF, JPY, PLN) if they don't already exist" + commands: + # First, get all existing currencies to alias them by code + - epcc get currencies + # Create currencies only if they don't already exist, and save aliases for cleanup + - | + epcc create currency --if-alias-does-not-exist code=EUR --save-as-alias created_currency=EUR code "EUR" exchange_rate 1 format "€{price}" decimal_point "." thousand_separator "," decimal_places 2 enabled true default false + epcc create currency --if-alias-does-not-exist code=GBP --save-as-alias created_currency=GBP code "GBP" exchange_rate 1 format "£{price}" decimal_point "." thousand_separator "," decimal_places 2 enabled true default false + epcc create currency --if-alias-does-not-exist code=CAD --save-as-alias created_currency=CAD code "CAD" exchange_rate 1 format "${price}" decimal_point "." thousand_separator "," decimal_places 2 enabled true default false + epcc create currency --if-alias-does-not-exist code=CHF --save-as-alias created_currency=CHF code "CHF" exchange_rate 1 format "CHF {price}" decimal_point "." thousand_separator "," decimal_places 2 enabled true default false + epcc create currency --if-alias-does-not-exist code=JPY --save-as-alias created_currency=JPY code "JPY" exchange_rate 1 format "¥{price}" decimal_point "." thousand_separator "," decimal_places 0 enabled true default false + epcc create currency --if-alias-does-not-exist code=PLN --save-as-alias created_currency=PLN code "PLN" exchange_rate 1 format "{price} zł" decimal_point "," thousand_separator " " decimal_places 2 enabled true default false + reset: + description: + short: "Remove currencies that were added by this runbook" + ignore_errors: true + commands: + # Only delete currencies that were actually created by this runbook + - | + epcc delete currency --if-alias-exists created_currency=EUR created_currency=EUR --ignore-errors + epcc delete currency --if-alias-exists created_currency=GBP created_currency=GBP --ignore-errors + epcc delete currency --if-alias-exists created_currency=CAD created_currency=CAD --ignore-errors + epcc delete currency --if-alias-exists created_currency=CHF created_currency=CHF --ignore-errors + epcc delete currency --if-alias-exists created_currency=JPY created_currency=JPY --ignore-errors + epcc delete currency --if-alias-exists created_currency=PLN created_currency=PLN --ignore-errors diff --git a/external/runbooks/manual-orders.epcc.yml b/external/runbooks/manual-orders.epcc.yml new file mode 100644 index 00000000..8f147218 --- /dev/null +++ b/external/runbooks/manual-orders.epcc.yml @@ -0,0 +1,202 @@ +# The name of the runbook is used in command lines for the 4th argument (e.g., epcc runbooks run hello-world) +name: "manual-orders" +description: + short: "A runbook for creating manual orders" + long: "A runbook for creating manual orders" +actions: + # The action name is used in command lines for the 4th argument (e.g., epcc runbook run hello-world create-customer) + create-orders: + variables: + currency-code: + type: ENUM:USD,EUR,GBP,CAD,CHF,JPY,PLN + default: "USD" + description: + short: "Currency code for the orders" + number-of-accounts: + type: INT + default: 0 + required: true + number-of-orders: + type: INT + default: 1 + description: + short: "Number of orders to create" + product-price-mean: + type: INT + default: "10000" + description: + short: "The average product price (in the smallest currency subunit)." + product-price-stddev: + type: INT + default: "3000" + description: + short: "The average product price (in the smallest currency subunit)." + number-of-products: + type: INT + default: 10 + description: + short: "The number of distinct products" + start-date: + type: STRING + # Default is 6 months ago + default: '{{ now | date_modify "-4320h" | date "2006-01-02" }}' + description: + short: "The earliest date of the order" + end-date: + type: STRING + default: '{{ now | date_modify "-6h" | date "2006-01-02" }}' + description: + short: "The start date of the order" + discount-chance: + type: INT + default: "10" + description: + short: "The chance of a discount being applied to an order (between 0 and 100)" + order-seed: + type: INT + default: '{{ pseudoRandInt 1 360000 }}' + description: + short: "The random seed to use when generating orders" + + accounts-seed: + type: INT + default: '1' + description: + short: "The random seed to use when generating accounts" + + products-seed: + type: INT + default: '1' + description: + short: "The random seed to use when generating products" + + + description: + short: "Create some addresses" + #language=gotemplate + commands: + + # Populate aliases + # We need to check if there are any accounts and products already created and if so not recreate them. + - | + {{ $products := index . "number-of-products" }} + {{ $pageSize := 25 }} + {{ $productPages := (div $products $pageSize) | int }} + {{ range untilStep 0 (add $productPages 1 | int) 1 -}} + {{ $offset := mul . $pageSize }} + epcc get -s pcm-products page[limit] 25 filter in(sku,{{ range untilStep 0 $pageSize 1 -}}{{ if gt . 0 }},{{ end }}mod-sku-{{ index $ "products-seed" }}-{{ add . $offset }}{{- end }}) + {{- end -}} + + {{ $accounts := index . "number-of-accounts" }} + {{ $accountPages := (div $accounts $pageSize) | int }} + {{ range untilStep 0 (add $accountPages 1 | int) 1 -}} + {{ $offset := mul . $pageSize }} + epcc get -s accounts page[limit] 25 filter in(external_ref,{{ range untilStep 0 $pageSize 1 -}}{{ if gt . 0 }},{{ end }}synth-acct-{{ index $ "accounts-seed" }}-{{ add . $offset }}{{- end }}) + {{- end -}} + + - | + - + {{ seed (index $ "accounts-seed") }} + {{ $accounts := index . "number-of-accounts" }} + {{ $accountNames := list }} + {{ $accountEmails := list }} + {{ $accountPasswords := list }} + {{- range untilStep 0 $accounts 1 }} + {{ $currentAccountName := fake "Name" }} + {{ $accountNames = append $accountNames $currentAccountName }} + epcc create account -s --if-alias-does-not-exist external_ref=synth-acct-{{ index $ "accounts-seed" }}-{{ . }} name '{{ $currentAccountName }}' external_ref 'synth-acct-{{ index $ "accounts-seed" }}-{{ . }}' + {{- end -}} + + {{ seed (index $ "products-seed") }} + {{ $orders := index . "number-of-orders" }} + {{ $productPriceMean := index . "product-price-mean" | float64 }} + {{ $productPriceStddev := index . "product-price-stddev" | float64 }} + {{ $products := index . "number-of-products" }} + {{ $productPrices := list }} + {{ $productNames := list}} + {{ $productSkus := list}} + {{- range untilStep 0 $products 1 }} + {{ $currentProductPrice := (pseudoRandNorm ( $productPriceMean | float64) $productPriceStddev | printf "%0.f" | max 100 ) }} + {{ $currentProductName := fake "ProductName" }} + {{ $currentProductSku := printf "mod-sku-%d-%d" (index $ "products-seed" ) . }} + {{ $productPrices = append $productPrices $currentProductPrice }} + {{ $productNames = append $productNames $currentProductName }} + {{ $productSkus = append $productSkus $currentProductSku }} + epcc create pcm-product -s --if-alias-does-not-exist sku=mod-sku-{{ index $ "products-seed" }}-{{ . }} name '{{ $currentProductName }}' description '{{ fake "ProductDescription"}}' sku 'mod-sku-{{ index $ "products-seed" }}-{{ . }}' commodity_type physical status live product_price {{ index $productPrices .}} + {{- end }} + + - + {{ seed (index $ "order-seed") }} + {{- range untilStep 0 $orders 1 }} + + {{ $ts := weightDatedTimeSample (index $ "start-date") (index $ "end-date") }} + {{ $totalWithoutTax := 0 -}} + {{ $totalTax := 0 -}} + + {{ $totalDiscount := 0 -}} + {{ $totalShipping := 0 -}} + {{ $totalShippingDiscount := 0 -}} + {{- $numberOfOrderItems := pseudoRandInt 1 (min $products 11 | int) -}} + {{- $productCurrency := index $ "currency-code" -}} + + {{ $giveDiscount := false }} + {{- if le (randInt 0 100) (index $ "discount-chance") }} + {{ $giveDiscount = true }} + {{- end }} + + {{ $orderItems := nRandInt $numberOfOrderItems 0 (len $productNames) }} + epcc create --save-as-alias order-{{ . }} -s manual-order --skip-alias-processing -s --auto-fill -- status complete payment paid shipping fulfilled debug_give_discount '{{ $giveDiscount }}' \ + {{ range untilStep 0 $numberOfOrderItems 1 -}} + {{- $productIdx := index $orderItems . }} + {{- $productName := index $productNames ($productIdx) }} + {{- $productPrice := index $productPrices $productIdx | int }} + {{- $productSku := index $productSkus $productIdx }} + {{- $productTax := 0 | int64 }} + + {{- $productDiscount := 0 | int64 }} + {{- if $giveDiscount }} + {{- $productDiscount = ( pseudoRandInt 2 10 | div $productPrice ) | int }} + {{- end }} + {{- $productPriceAfterDiscount := sub $productPrice $productDiscount | int }} + {{- $productPriceWithTax := add $productPriceAfterDiscount $productTax | int64 }} + {{- $quantity := 2 }} + {{- if ge (randInt 1 100) 80 }} + {{- $quantity = add $quantity (randInt 1 3) }} + {{- end }} + {{- if ge (randInt 1 100) 95 }} + {{- $quantity = add $quantity (randInt 4 6) }} + {{- end }} + {{- $totalWithoutTax = add $totalWithoutTax (mul $productPriceAfterDiscount $quantity) }} + + {{- $totalTax = add $totalTax (mul $productTax $quantity) }} + {{- $totalDiscount = add $totalDiscount (mul $productDiscount $quantity) -}} + included.items[{{.}}].meta.timestamps.created_at "{{ $ts }}" \ + included.items[{{.}}].id '{{ uuidv4 }}' included.items[{{.}}].quantity {{ $quantity }} included.items[{{.}}].product_id sku={{ $productSku }} included.items[{{.}}].name "{{ $productName }}" included.items[{{.}}].sku {{ $productSku }} \ + included.items[{{.}}].unit_price.amount {{ $productPrice }} included.items[{{.}}].unit_price.currency {{ $productCurrency }} included.items[{{.}}].unit_price.includes_tax false \ + included.items[{{.}}].value.amount {{ mul $productPrice $quantity }} included.items[{{.}}].value.currency {{ $productCurrency }} included.items[{{.}}].value.includes_tax false \ + included.items[{{.}}].meta.display_price.with_tax.unit.amount {{ $productPriceWithTax }} included.items[{{.}}].meta.display_price.with_tax.unit.formatted '{{ $productPriceWithTax | formatPrice $productCurrency }}' included.items[{{.}}].meta.display_price.with_tax.unit.currency {{ $productCurrency }} \ + included.items[{{.}}].meta.display_price.with_tax.value.amount {{ mul $quantity $productPriceWithTax }} included.items[{{.}}].meta.display_price.with_tax.value.formatted '{{ (mul $quantity $productPriceWithTax) | formatPrice $productCurrency }}' included.items[{{.}}].meta.display_price.with_tax.value.currency {{ $productCurrency }} \ + included.items[{{.}}].meta.display_price.without_tax.unit.amount {{ $productPrice }} included.items[{{.}}].meta.display_price.without_tax.unit.formatted '{{ $productPrice | formatPrice $productCurrency }}' included.items[{{.}}].meta.display_price.without_tax.unit.currency {{ $productCurrency }} \ + included.items[{{.}}].meta.display_price.without_tax.value.amount {{ mul $quantity $productPrice }} included.items[{{.}}].meta.display_price.without_tax.value.formatted '{{ (mul $quantity $productPrice) | formatPrice $productCurrency }}' included.items[{{.}}].meta.display_price.without_tax.value.currency {{ $productCurrency }} \ + included.items[{{.}}].meta.display_price.tax.unit.amount {{ $productTax }} included.items[{{.}}].meta.display_price.tax.unit.formatted '{{ $productTax | formatPrice $productCurrency }}' included.items[{{.}}].meta.display_price.tax.unit.currency {{ $productCurrency }} \ + included.items[{{.}}].meta.display_price.tax.value.amount {{ mul $quantity $productTax }} included.items[{{.}}].meta.display_price.tax.value.formatted '{{ (mul $productTax $quantity) | formatPrice $productCurrency }}' included.items[{{.}}].meta.display_price.tax.value.currency {{ $productCurrency }} \ + included.items[{{.}}].meta.display_price.discount.unit.amount -{{ $productDiscount }} included.items[{{.}}].meta.display_price.discount.unit.formatted '-{{ $productDiscount | formatPrice $productCurrency }}' included.items[{{.}}].meta.display_price.discount.unit.currency {{ $productCurrency }} \ + included.items[{{.}}].meta.display_price.discount.value.amount -{{ mul $quantity $productDiscount }} included.items[{{.}}].meta.display_price.discount.value.formatted '-{{ (mul $quantity $productDiscount) | formatPrice $productCurrency }}' included.items[{{.}}].meta.display_price.discount.value.currency {{ $productCurrency }} \ + included.items[{{.}}].meta.display_price.without_discount.unit.amount {{ $productPrice }} included.items[{{.}}].meta.display_price.without_discount.unit.formatted '{{ $productPrice | formatPrice $productCurrency }}' included.items[{{.}}].meta.display_price.without_discount.unit.currency {{ $productCurrency }} \ + included.items[{{.}}].meta.display_price.without_discount.value.amount {{ mul $quantity $productPrice }} included.items[{{.}}].meta.display_price.without_discount.value.formatted '{{ (mul $quantity $productPrice) | formatPrice $productCurrency }}' included.items[{{.}}].meta.display_price.without_discount.value.currency {{ $productCurrency }} \ + {{ end }} + {{- $totalWithTax := add $totalTax $totalWithoutTax -}} + {{- $totalBeforeDiscounts := add $totalDiscount $totalWithoutTax -}} + meta.display_price.without_tax.amount {{ $totalWithoutTax }} meta.display_price.without_tax.formatted '{{ $totalWithoutTax | formatPrice $productCurrency }}' meta.display_price.without_tax.currency {{ $productCurrency }} \ + meta.display_price.tax.amount {{ $totalTax }} meta.display_price.tax.formatted '{{ $totalTax | formatPrice $productCurrency }}' meta.display_price.tax.currency {{ $productCurrency }} \ + meta.display_price.with_tax.amount {{ $totalWithTax }} meta.display_price.with_tax.formatted '{{ $totalWithTax | formatPrice $productCurrency }}' meta.display_price.with_tax.currency {{ $productCurrency }} \ + meta.display_price.paid.amount {{ $totalWithTax }} meta.display_price.paid.formatted '{{ $totalWithTax | formatPrice $productCurrency }}' meta.display_price.paid.currency {{ $productCurrency }} \ + meta.display_price.balance_owing.amount 0 meta.display_price.balance_owing.formatted '{{ 0 | formatPrice $productCurrency }}' meta.display_price.balance_owing.currency {{ $productCurrency }} \ + meta.display_price.discount.amount -{{ $totalDiscount }} meta.display_price.discount.formatted '-{{ $totalDiscount | formatPrice $productCurrency }}' meta.display_price.discount.currency {{ $productCurrency }} \ + meta.display_price.authorized.amount 0 meta.display_price.authorized.formatted '{{ 0 | formatPrice $productCurrency }}' meta.display_price.authorized.currency {{ $productCurrency }} \ + meta.display_price.without_discount.amount {{ $totalBeforeDiscounts }} meta.display_price.without_discount.formatted '{{ $totalBeforeDiscounts | formatPrice $productCurrency }}' meta.display_price.without_discount.currency {{ $productCurrency }} \ + meta.display_price.shipping.amount {{ $totalShipping }} meta.display_price.shipping.formatted '{{ $totalShippingDiscount | formatPrice $productCurrency }}' meta.display_price.shipping.currency {{ $productCurrency }} \ + meta.display_price.shipping_discount.amount {{ $totalShippingDiscount }} meta.display_price.shipping_discount.formatted '{{ $totalShippingDiscount | formatPrice $productCurrency }}' meta.display_price.shipping_discount.currency {{ $productCurrency }} \ + meta.timestamps.created_at "{{ $ts }}" + {{- end -}} + diff --git a/external/runbooks/rule-promotions.epcc.yml b/external/runbooks/rule-promotions.epcc.yml index e092b398..44de4d3b 100644 --- a/external/runbooks/rule-promotions.epcc.yml +++ b/external/runbooks/rule-promotions.epcc.yml @@ -73,6 +73,8 @@ actions: create-cart-and-add-ranges: + description: + short: "Create a cart with ranges" variables: cart-id: type: STRING @@ -94,6 +96,20 @@ actions: - | epcc get cart test-cart include items --output-jq '.included.items[] | " CART ITEM: name: \\(.name), sku: \\(.sku), price: \\(.meta.display_price.without_discount.value.formatted), discount: \\(.meta.display_price.discount.value.formatted), total: \\(.meta.display_price.with_tax.value.formatted)"' + display-cart: + description: + short: "Display a cart" + variables: + cart-id: + type: RESOURCE_ID:carts + commands: + - | + epcc get cart "{{ (index . "cart-id") }}" --output-jq '.data | "CART: name: \'\\(.name)\', id: \\(.id), price: \\(.meta.display_price.without_discount.formatted), discount: \\(.meta.display_price.discount.formatted), total: \\(.meta.display_price.with_tax.formatted)"' + - | + epcc get cart "{{ (index . "cart-id") }}" include items --output-jq '.included.items[] | " CART ITEM: name: \'\\(.name)\', sku: \\(.sku), type: \\(.type), unit_price: \\(.meta.display_price.without_discount.unit.formatted), unit_discount: \\(.meta.display_price.discount.unit.formatted), unit_total: total: \\(.meta.display_price.with_tax.unit.formatted), quantity: \\(.quantity), total: \\(.meta.display_price.with_tax.value.formatted)"' + + + #' diff --git a/external/runbooks/run-all-runbooks.sh b/external/runbooks/run-all-runbooks.sh index 6c4521a1..ed0ee666 100755 --- a/external/runbooks/run-all-runbooks.sh +++ b/external/runbooks/run-all-runbooks.sh @@ -13,13 +13,21 @@ set -x #Let's test that epcc command works after an embarrassing bug that caused it to panic :( epcc + +echo "Starting Currencies Runbook" +epcc reset-store .+ +epcc runbooks run currencies add-currencies +epcc runbooks run currencies reset + echo "Starting Rule Promotions Runbook" epcc reset-store .+ epcc runbooks run rule-promotions-how-to create-prequisites epcc runbooks run rule-promotions-how-to create-rule-promotions epcc runbooks run rule-promotions-how-to create-cart-and-add-ranges +epcc runbooks run rule-promotions-how-to display-cart --cart-id "test-cart" epcc runbooks run rule-promotions-how-to reset + echo "Starting Multi Location Inventory Runbook" epcc reset-store .+ epcc headers set ep-inventories-multi-location true @@ -126,6 +134,46 @@ epcc runbooks run customer-cart-associations create-customers-and-carts-with-cus epcc runbooks run customer-cart-associations delete-customer-and-carts-with-custom-items epcc runbooks run customer-cart-associations reset +echo "Starting Manual Orders Runbook" +epcc reset-store .+ +epcc aliases clear +sleep 1.5 +NUMBER_OF_ORDERS=$(epcc get orders --output-jq .meta.results.total) +epcc runbooks run manual-orders create-orders --number-of-accounts 3 --number-of-products 4 --number-of-orders 7 +epcc runbooks run manual-orders create-orders --number-of-accounts 3 --number-of-products 4 --number-of-orders 6 + +# Allow for eventual consistency. +sleep 1.5 +NUMBER_OF_ORDERS_NOW=$(epcc get orders --output-jq .meta.results.total) + +EXPECTED_DIFF=13 +ACTUAL_DIFF=$((NUMBER_OF_ORDERS_NOW - NUMBER_OF_ORDERS)) + +if [ "$ACTUAL_DIFF" -eq "$EXPECTED_DIFF" ]; then + echo "✅ Correct number of orders added: $ACTUAL_DIFF" +else + echo "❌ Expected to add $EXPECTED_DIFF orders, but added $ACTUAL_DIFF" + exit 1 +fi + +# Check number of accounts +NUM_ACCOUNTS=$(epcc get accounts --output-jq .meta.results.total) +if [ "$NUM_ACCOUNTS" -eq 3 ]; then + echo "✅ Correct number of accounts: 3" +else + echo "❌ Expected 3 accounts, but found $NUM_ACCOUNTS" + exit 1 +fi + +# Check number of products +NUM_PRODUCTS=$(epcc get pcm-products --output-jq .meta.results.total) +if [ "$NUM_PRODUCTS" -eq 4 ]; then + echo "✅ Correct number of products: 4" +else + echo "❌ Expected 4 products, but found $NUM_PRODUCTS" + exit 1 +fi + echo "SUCCESS" diff --git a/external/runbooks/runbook_rendering.go b/external/runbooks/runbook_rendering.go index 0cc3baf2..1c98f4bc 100644 --- a/external/runbooks/runbook_rendering.go +++ b/external/runbooks/runbook_rendering.go @@ -46,6 +46,20 @@ func RenderTemplates(templateName string, rawCmd string, stringVars map[string]* data[key] = val } else if strings.HasPrefix(variableDef.Type, "RESOURCE_ID:") { data[key] = val + } else if strings.HasPrefix(variableDef.Type, "ENUM:") { + // ENUM types are treated as strings, validate the value is one of the enum options + enumValues := strings.Split(variableDef.Type[5:], ",") + validValue := false + for _, enumVal := range enumValues { + if *val == enumVal { + validValue = true + break + } + } + if !validValue { + return nil, fmt.Errorf("error processing variable %s, value %q is not a valid enum option. Valid options are: [%s]", key, *val, strings.Join(enumValues, ", ")) + } + data[key] = val } else { return nil, fmt.Errorf("error processing variable %s, unknown type [%s] specified in template", key, variableDef.Type) } diff --git a/external/templates/funcs.go b/external/templates/funcs.go index 526fd02e..f12d5b77 100644 --- a/external/templates/funcs.go +++ b/external/templates/funcs.go @@ -5,6 +5,7 @@ import ( "math" "math/rand" "sort" + "strings" "sync" "time" @@ -132,16 +133,82 @@ func NRandInt(nAny, minAny, maxAny any) []int { } } +type CurrencyConfig struct { + DecimalPlaces int + DecimalPoint string + ThousandSeparator string + Format string +} + +var currencyConfigs = map[string]CurrencyConfig{ + "USD": {DecimalPlaces: 2, DecimalPoint: ".", ThousandSeparator: ",", Format: "${price}"}, + "EUR": {DecimalPlaces: 2, DecimalPoint: ".", ThousandSeparator: ",", Format: "€{price}"}, + "GBP": {DecimalPlaces: 2, DecimalPoint: ".", ThousandSeparator: ",", Format: "£{price}"}, + "CAD": {DecimalPlaces: 2, DecimalPoint: ".", ThousandSeparator: ",", Format: "${price}"}, + "CHF": {DecimalPlaces: 2, DecimalPoint: ".", ThousandSeparator: ",", Format: "CHF {price}"}, + "JPY": {DecimalPlaces: 0, DecimalPoint: ".", ThousandSeparator: ",", Format: "¥{price}"}, + "PLN": {DecimalPlaces: 2, DecimalPoint: ",", ThousandSeparator: " ", Format: "{price} zł"}, +} + func FormatPrice(currency string, pAny any) string { + amount := toInt64(pAny) + + // Get currency config, default to USD if not found + config, ok := currencyConfigs[currency] + if !ok { + config = currencyConfigs["USD"] + } + + // Handle negative amounts + isNegative := amount < 0 + if isNegative { + amount = -amount + } + + // Calculate floated value: amount / 10^decimal_places + divisor := int64(math.Pow10(config.DecimalPlaces)) + floatedValue := float64(amount) / float64(divisor) - p := toInt64(pAny) + // Format the number with proper separators + formattedNumber := formatNumber(floatedValue, config.DecimalPlaces, config.DecimalPoint, config.ThousandSeparator) + + // Add negative sign if needed + if isNegative { + formattedNumber = "-" + formattedNumber + } + + // Replace {price} in format string (similar to money.go logic) + return strings.Replace(config.Format, "{price}", formattedNumber, -1) +} + +func formatNumber(value float64, precision int, decimalPoint string, thousandSeparator string) string { + // Round to precision + multiplier := math.Pow10(precision) + rounded := math.Round(value * multiplier) + intPart := int64(rounded / multiplier) + fracPart := int64(rounded) - intPart*int64(multiplier) + + // Format integer part with thousand separators + intStr := fmt.Sprintf("%d", intPart) + if intPart < 0 { + intStr = fmt.Sprintf("%d", -intPart) + } + + // Add thousand separators + var formatted string + for i, digit := range intStr { + if i > 0 && (len(intStr)-i)%3 == 0 && thousandSeparator != "" { + formatted += thousandSeparator + } + formatted += string(digit) + } - symbol := "£" - if currency == "USD" { - symbol = "$" + // Add decimal part if precision > 0 + if precision > 0 { + formatted += decimalPoint + fmt.Sprintf("%0*d", precision, fracPart) } - return fmt.Sprintf("%s%d.%02d", symbol, p/100, p%100) + return formatted } func WeightedDateTimeSampler(start string, end string) string {