diff --git a/docs/resources/stripe_price.md b/docs/resources/stripe_price.md index 1c6d63c3..4bdcd9c4 100644 --- a/docs/resources/stripe_price.md +++ b/docs/resources/stripe_price.md @@ -17,7 +17,7 @@ represented by prices. This approach lets you change prices without having to ch For example, you might have a single "gold" product that has prices for $10/month, $100/year, and €9 once. ~> Removal of the price isn't supported through the Stripe SDK. The best practice, which this provider follows, -is to archive the price by marking it as inactive on destroy, which indicates that the price is not longer +is to archive the price by marking it as inactive on destroy, which indicates that the price is no longer available for purchase. ## Example Usage diff --git a/docs/resources/stripe_shipping_rate.md b/docs/resources/stripe_shipping_rate.md new file mode 100644 index 00000000..20988733 --- /dev/null +++ b/docs/resources/stripe_shipping_rate.md @@ -0,0 +1,140 @@ +--- +layout: "stripe" +page_title: "Stripe: stripe_shipping_rate" +description: |- The Stripe Shipping Rate can be created, modified and configured by this resource. +--- + +# stripe_shipping_rate + +With this resource, you can create a shipping rate - [Stripe API price documentation](https://stripe.com/docs/api/shipping_rates). + + +Shipping rates let you display various shipping options—like standard, express, and overnight—with more accurate delivery estimates. +Charge your customer for shipping using different Stripe products, some of which require coding. + +~> Removal of the shipping rate isn't supported through the Stripe SDK. The best practice, which this provider follows, +is to archive the shipping rate by marking it as inactive on destroy, which indicates that the shipping rate is no longer +available. + +## Example Usage + +```hcl +// minimal shipping rate +resource "stripe_shipping_rate" "shipping_rate" { + display_name = "minimal shipping rate" + fixed_amount { + amount = 1000 + currency = "aud" + } +} + +// shipping rate with delivery estimate +resource "stripe_shipping_rate" "shipping_rate" { + display_name = "shipping rate" + fixed_amount { + amount = 1000 + currency = "aud" + } + + delivery_estimate { + minimum { + unit = "hour" + value = 24 + } + maximum { + unit = "day" + value = 4 + } + } +} + +// shipping rate with currency options +// !!! Currency options have to be sorted alphabetically by the currency field +resource "stripe_shipping_rate" "shipping" { + display_name = "shipping rate" + fixed_amount { + amount = 1000 + currency = "aud" + + currency_option { + currency = "eur" + amount = 350 + } + currency_option { + currency = "usd" + amount = 500 + } + } +} + +``` + +## Argument Reference + +Arguments accepted by this resource include: + +* `type` - (Optional) String. The type of calculation to use on the shipping rate. Can only be `fixed_amount` for now. +* `display_name` - (Required) String. The name of the shipping rate, meant to be displayable to the customer. + This will appear on CheckoutSessions. +* `fixed_amount` - (Required) List(Resource). Describes a fixed amount to charge for shipping. + Must be present if type is `fixed_amount`. For details of individual arguments see [Fixed Amount](#fixed-amount). +* `delivery_estimate` - (Optional) List(Resource). The estimated range for how long shipping will take, + meant to be displayable to the customer. This will appear on CheckoutSessions. + For details please see [Delivery Estimate](#delivery-estimate). +* `active` - (Optional) Bool. Whether the shipping rate is active (can't be used when creating). Defaults to `true`. +* `tax_behaviour` - (Optional) String. Specifies whether the price is considered inclusive of taxes or exclusive of + taxes. One of `inclusive`, `exclusive`, or `unspecified`. Once specified it cannot be changed, default is `unspecified`. +* `tax_code` - (Optional) String. A tax code ID. The Shipping tax code is `txcd_92010001`. +* `metadata` - (Optional) Map(String). Set of key-value pairs that you can attach to an object. This can be useful for + storing additional information about the object in a structured format. + +### Fixed Amount + +`fixed_amount` Supports the following arguments: + +* `currency` - (Required) String. Three-letter ISO currency code, in lowercase - [supported currencies](https://stripe.com/docs/currencies). +* `amount` - (Required) Int. A non-negative integer in cents representing how much to charge. +* `currency_option` - (Optional) List(Resource). Please see argument details [Currency Option](#currency-option) + +### Currency Option + +`currency_option` Can be used multiple times within the `fixed_amount` part. +~> When multiple currency_options are defined sorting by currency field is mandatory! +Otherwise, the provider consider next run as a change. + +Currency option support the following arguments: + +* `currency` - (Required) String. Three-letter ISO currency code, in lowercase - [supported currencies](https://stripe.com/docs/currencies). +* `amount` - (Required) Int. (Required) Int. A non-negative integer in cents representing how much to charge. +* `tax_behaviour` - (Optional) String. Specifies whether the price is considered inclusive of taxes or exclusive of + taxes. One of `inclusive`, `exclusive`, or `unspecified`. Once specified it cannot be changed, default is `unspecified`. + +### Delivery Estimate + +`delivery_estimate` Supports the following arguments: + +* `minimum` - (Required) List(Resource). The lower bound of the estimated range. + Please see [Delivery Estimate Definition](#delivery-estimate-definition). +* `maximum` - (Required) List(Resource. The upper bound of the estimated range. + Please see [Delivery Estimate Definition](#delivery-estimate-definition). + +### Delivery Estimate Definition + +`maximum` and `minimum` share the same definition: + +* `unit` - (Required) String. A unit of time. Possible values `hour`, `day`, `business_day`, `week` and `month`. +* `value` - (Required) Int. Must be greater than 0. + +## Attribute Reference + +Attributes exported by this resource include: + +* `id` - String. The unique identifier for the object. +* `type` - String. The type of calculation to use on the shipping rate. +* `display_name` - String. The name of the shipping rate, meant to be displayable to the customer. +* `active` - Bool. Whether the shipping rate can be used. +* `fixed_amount` - List(Resource). Describes a fixed amount to charge for shipping. +* `delivery_estimate` - List(Resource). The estimated range for how long shipping will take, meant to be displayable to the customer. +* `tax_behaviour` - String. Specifies whether the price is considered inclusive of taxes or exclusive of taxes. +* `tax_code` - String. A tax code ID. +* `livemode` - Bool. Has the value true if the object exists in live mode or the value false if the object exists in test mode. \ No newline at end of file diff --git a/stripe/provider.go b/stripe/provider.go index c11406b4..30019329 100644 --- a/stripe/provider.go +++ b/stripe/provider.go @@ -28,6 +28,7 @@ func Provider() *schema.Provider { "stripe_price": resourceStripePrice(), "stripe_customer": resourceStripeCustomer(), "stripe_tax_rate": resourceStripeTaxRate(), + "stripe_shipping_rate": resourceStripeShippingRate(), "stripe_portal_configuration": resourceStripePortalConfiguration(), }, ConfigureContextFunc: providerConfigure, diff --git a/stripe/resource_shipping_rate.go b/stripe/resource_shipping_rate.go new file mode 100644 index 00000000..6532a37f --- /dev/null +++ b/stripe/resource_shipping_rate.go @@ -0,0 +1,383 @@ +package stripe + +import ( + "context" + "sort" + + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/stripe/stripe-go/v76" + "github.com/stripe/stripe-go/v76/client" +) + +func resourceStripeShippingRate() *schema.Resource { + return &schema.Resource{ + ReadContext: resourceStripeShippingRateRead, + CreateContext: resourceStripeShippingRateCreate, + UpdateContext: resourceStripeShippingRateUpdate, + DeleteContext: resourceStripeShippingRateDelete, + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Schema: map[string]*schema.Schema{ + "id": { + Type: schema.TypeString, + Computed: true, + Description: "Unique identifier for the object.", + }, + "type": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: "fixed_amount", + Description: "The type of calculation to use on the shipping rate. " + + "Can only be fixed_amount for now", + }, + "display_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The name of the shipping rate, meant to be displayable to the customer. " + + "This will appear on CheckoutSessions.", + }, + "active": { + Type: schema.TypeBool, + Optional: true, + Default: true, + Description: "Whether the shipping rate can be used for new purchases. Defaults to true.", + }, + "fixed_amount": { + Type: schema.TypeList, + Required: true, + ForceNew: true, + MaxItems: 1, + Description: "Describes a fixed amount to charge for shipping. Must be present for now.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "amount": { + Type: schema.TypeInt, + Required: true, + Description: "A non-negative integer in cents representing how much to charge.", + }, + "currency": { + Type: schema.TypeString, + Required: true, + Description: "Three-letter ISO currency code, in lowercase. Must be a supported currency.", + }, + "currency_option": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Description: "Shipping rates defined in each available currency option. " + + "Each key must be a three-letter ISO currency code and a supported currency. " + + "For example, to get your shipping rate in eur, " + + "fetch the value of the eur key in currency_options. " + + "This field is not included by default. " + + "To include it in the response, expand the currency_options field.", + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "currency": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Three-letter ISO currency code, in lowercase. Must be a supported currency.", + }, + "amount": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "A non-negative integer in cents representing how much to charge.", + }, + "tax_behavior": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Default: stripe.PriceTaxBehaviorUnspecified, + Description: "Specifies whether the rate is considered inclusive of taxes or " + + "exclusive of taxes. One of inclusive, exclusive, or unspecified. ", + }, + }, + }, + }, + }, + }, + }, + "delivery_estimate": { + Type: schema.TypeList, + Optional: true, + ForceNew: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "minimum": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "unit": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The lower bound of the estimated range. " + + "If empty, represents no lower bound.", + }, + "value": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + Description: "Must be greater than 0.", + }, + }, + }, + }, + "maximum": { + Type: schema.TypeList, + MaxItems: 1, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "unit": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The upper bound of the estimated range. " + + "If empty, represents no lower bound.", + }, + "value": { + Type: schema.TypeInt, + ForceNew: true, + Required: true, + Description: "Must be greater than 0.", + }, + }, + }, + }, + }, + }, + }, + "tax_behavior": { + Type: schema.TypeString, + Optional: true, + Default: stripe.PriceTaxBehaviorUnspecified, + Description: "Specifies whether the rate is considered inclusive of taxes or " + + "exclusive of taxes. One of inclusive, exclusive, or unspecified. ", + }, + "livemode": { + Type: schema.TypeBool, + Computed: true, + Description: "Has the value true if the object exists in live mode or the value false " + + "if the object exists in test mode.", + }, + "tax_code": { + Type: schema.TypeString, + Optional: true, + ForceNew: true, + Description: "A tax code ID. The Shipping tax code is txcd_92010001.", + }, + "metadata": { + Type: schema.TypeMap, + Optional: true, + Elem: &schema.Schema{Type: schema.TypeString}, + Description: "Set of key-value pairs that you can attach to an object. " + + "This can be useful for storing additional information about the object in a structured format.", + }, + }, + } +} + +func resourceStripeShippingRateRead(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(*client.API) + var shippingRate *stripe.ShippingRate + var err error + + params := &stripe.ShippingRateParams{} + params.AddExpand("fixed_amount.currency_options") + err = retryWithBackOff(func() error { + shippingRate, err = c.ShippingRates.Get(d.Id(), params) + return err + }) + if err != nil { + return diag.FromErr(err) + } + + return CallSet( + d.Set("type", shippingRate.Type), + d.Set("display_name", shippingRate.DisplayName), + d.Set("active", shippingRate.Active), + func() error { + if shippingRate.FixedAmount != nil { + fixedAmount := map[string]interface{}{ + "amount": shippingRate.FixedAmount.Amount, + "currency": shippingRate.FixedAmount.Currency, + } + + if len(shippingRate.FixedAmount.CurrencyOptions) > 0 { + var options []map[string]interface{} + for currency, currencyOptions := range shippingRate.FixedAmount.CurrencyOptions { + if currency == string(shippingRate.FixedAmount.Currency) { + continue // don't add the same currency into the currency options + } + options = append(options, map[string]interface{}{ + "currency": currency, + "amount": currencyOptions.Amount, + "tax_behavior": currencyOptions.TaxBehavior, + }) + } + sort.Slice(options, func(i, j int) bool { + return ToString(options[i]["currency"]) < ToString(options[j]["currency"]) + }) + fixedAmount["currency_option"] = options + } + + return d.Set("fixed_amount", []map[string]interface{}{fixedAmount}) + } + return nil + }(), + func() error { + if shippingRate.DeliveryEstimate != nil { + deliveryEstimate := make(map[string]interface{}) + if shippingRate.DeliveryEstimate.Minimum != nil { + deliveryEstimate["minimum"] = []map[string]interface{}{ + { + "unit": shippingRate.DeliveryEstimate.Minimum.Unit, + "value": shippingRate.DeliveryEstimate.Minimum.Value, + }, + } + } + + if shippingRate.DeliveryEstimate.Maximum != nil { + deliveryEstimate["maximum"] = []map[string]interface{}{ + { + "unit": shippingRate.DeliveryEstimate.Maximum.Unit, + "value": shippingRate.DeliveryEstimate.Maximum.Value, + }, + } + } + return d.Set("delivery_estimate", []map[string]interface{}{deliveryEstimate}) + } + return nil + }(), + d.Set("tax_behavior", shippingRate.TaxBehavior), + d.Set("livemode", shippingRate.Livemode), + d.Set("tax_code", shippingRate.TaxCode), + d.Set("metadata", shippingRate.Metadata), + ) +} + +func resourceStripeShippingRateCreate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(*client.API) + var shippingRate *stripe.ShippingRate + var err error + + params := &stripe.ShippingRateParams{ + Type: stripe.String(ExtractString(d, "type")), + DisplayName: stripe.String(ExtractString(d, "display_name")), + } + + if fixedAmount, set := d.GetOk("fixed_amount"); set { + fixedAmountMap := ToMap(fixedAmount) + params.FixedAmount = &stripe.ShippingRateFixedAmountParams{ + Amount: stripe.Int64(ToInt64(fixedAmountMap["amount"])), + Currency: stripe.String(ToString(fixedAmountMap["currency"])), + } + if _, set := fixedAmountMap["currency_option"]; set { + params.FixedAmount.CurrencyOptions = make(map[string]*stripe.ShippingRateFixedAmountCurrencyOptionsParams) + for _, options := range ToMapSlice(fixedAmountMap["currency_option"]) { + params.FixedAmount.CurrencyOptions[ToString(options["currency"])] = &stripe.ShippingRateFixedAmountCurrencyOptionsParams{ + Amount: stripe.Int64(ToInt64(options["amount"])), + TaxBehavior: stripe.String(ToString(options["tax_behavior"])), + } + } + } + } + + if deliveryEstimate, set := d.GetOk("delivery_estimate"); set { + params.DeliveryEstimate = &stripe.ShippingRateDeliveryEstimateParams{} + delivery := ToMap(deliveryEstimate) + if minimumDelivery, set := delivery["minimum"]; set { + minimum := ToMap(minimumDelivery) + params.DeliveryEstimate.Minimum = &stripe.ShippingRateDeliveryEstimateMinimumParams{ + Value: stripe.Int64(ToInt64(minimum["value"])), + } + if _, set := minimum["unit"]; set { + params.DeliveryEstimate.Minimum.Unit = stripe.String(ToString(minimum["unit"])) + } + + } + if maximumDelivery, set := delivery["maximum"]; set { + maximum := ToMap(maximumDelivery) + params.DeliveryEstimate.Maximum = &stripe.ShippingRateDeliveryEstimateMaximumParams{ + Value: stripe.Int64(ToInt64(maximum["value"])), + } + if _, set := maximum["unit"]; set { + params.DeliveryEstimate.Maximum.Unit = stripe.String(ToString(maximum["unit"])) + } + } + } + + if taxBehavior, set := d.GetOk("tax_behavior"); set { + params.TaxBehavior = stripe.String(ToString(taxBehavior)) + } + if taxCode, set := d.GetOk("tax_code"); set { + params.TaxCode = stripe.String(ToString(taxCode)) + } + if meta, set := d.GetOk("metadata"); set { + for k, v := range ToMap(meta) { + params.AddMetadata(k, ToString(v)) + } + } + + err = retryWithBackOff(func() error { + shippingRate, err = c.ShippingRates.New(params) + return err + }) + if err != nil { + return diag.FromErr(err) + } + d.SetId(shippingRate.ID) + return resourceStripeShippingRateRead(ctx, d, m) +} + +func resourceStripeShippingRateUpdate(ctx context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(*client.API) + var err error + + params := &stripe.ShippingRateParams{} + if d.HasChange("active") { + params.Active = stripe.Bool(ExtractBool(d, "active")) + } + if d.HasChange("tax_behavior") { + params.TaxBehavior = stripe.String(ExtractString(d, "tax_behavior")) + } + if d.HasChange("metadata") { + params.Metadata = nil + UpdateMetadata(d, params) + } + + err = retryWithBackOff(func() error { + _, err = c.ShippingRates.Update(d.Id(), params) + return err + }) + if err != nil { + return diag.FromErr(err) + } + + return resourceStripeShippingRateRead(ctx, d, m) +} + +func resourceStripeShippingRateDelete(_ context.Context, d *schema.ResourceData, m interface{}) diag.Diagnostics { + c := m.(*client.API) + var err error + + params := &stripe.ShippingRateParams{ + Active: stripe.Bool(false), + } + + err = retryWithBackOff(func() error { + _, err = c.ShippingRates.Update(d.Id(), params) + return err + }) + + d.SetId("") + return nil +} diff --git a/stripe/resource_stripe_price.go b/stripe/resource_stripe_price.go index e2e273fe..cb702de9 100644 --- a/stripe/resource_stripe_price.go +++ b/stripe/resource_stripe_price.go @@ -750,7 +750,7 @@ func resourceStripePriceUpdate(ctx context.Context, d *schema.ResourceData, m in } err = retryWithBackOff(func() error { - _, err := c.Prices.Update(d.Id(), params) + _, err = c.Prices.Update(d.Id(), params) return err }) if err != nil {