{"record":{"id":"949db8eafe508834","repo":"antiwork/gumroad","slug":"sorry-shipping-destinations-have-to-be-unique","errorCode":null,"errorMessage":"Sorry, shipping destinations have to be unique.","messagePattern":"Sorry, shipping destinations have to be unique\\.","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":null,"severity":"error","filePath":"app/models/link.rb","lineNumber":950,"sourceCode":"    end\n  end\n\n  def removed_file_info_attributes\n    removed_file_info_attributes = self.json_data.present? ? self.json_data[\"removed_file_info_attributes\"] : []\n    if removed_file_info_attributes.present?\n      removed_file_info_attributes.map(&:to_sym)\n    else\n      []\n    end\n  end\n\n  def save_shipping_destinations!(shipping_destinations)\n    shipping_destinations ||= []\n    deduped_destinations = shipping_destinations.uniq { |destination| destination[\"country_code\"] }\n\n    if deduped_destinations.size != shipping_destinations.size\n      errors.add(:base, \"Sorry, shipping destinations have to be unique.\")\n      raise LinkInvalid, \"Sorry, shipping destinations have to be unique.\"\n    end\n\n    remaining_shipping_destinations = self.shipping_destinations.alive.pluck(:id)\n\n    # Cannot empty out shipping destinations for a published physical product\n    if alive? && (shipping_destinations.empty? || shipping_destinations.first == \"\")\n      errors.add(:base, \"The product needs to be shippable to at least one destination.\")\n      raise LinkInvalid, \"The product needs to be shippable to at least one destination.\"\n    end\n\n    shipping_destinations.each do |destination|\n      next if destination.try(:[], \"country_code\").blank?\n\n      shipping_destination = ShippingDestination.find_or_create_by(country_code: destination[\"country_code\"], link_id: id)\n      # TODO: :product_edit_react cleanup\n      one_item_rate_cents = destination[\"one_item_rate_cents\"]\n      multiple_items_rate_cents = destination[\"multiple_items_rate_cents\"]\n      one_item_rate_cents ||= string_to_price_cents(price_currency_type, destination[\"one_item_rate\"])","sourceCodeStart":932,"sourceCodeEnd":968,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/models/link.rb#L932-L968","documentation":"Raised as Link::LinkInvalid (app/models/link.rb:1702) by Link#save_shipping_destinations! when the submitted destination list contains two entries with the same country_code (uniq by country_code shrinks the list). One shipping rate row per country per product is the data model's invariant, so duplicates are rejected before any ShippingDestination find_or_create runs.","triggerScenarios":"POST/PATCH product params whose shipping_destinations array includes e.g. two {'country_code' => 'US'} entries — typically a UI bug appending an edited row instead of replacing it, or a client merging destination lists.","commonSituations":"React/jQuery shipping table letting the seller pick the same country twice; spreadsheet/CSV import concatenating rows; duplicate form submission re-appending the row.","solutions":["Dedupe by country_code client-side before submit — replace the existing row for that country instead of adding a second.","Prevent selecting an already-chosen country in the destination picker.","Server-side callers can shipping_destinations.uniq { |d| d['country_code'] } before invoking save_shipping_destinations! when last-write-wins is acceptable.","Rescue Link::LinkInvalid and show errors.full_messages."],"exampleFix":"# before\nlink.save_shipping_destinations!(params[:shipping_destinations]) # LinkInvalid: duplicates\n\n# after\nsave_shipping_destinations!(params[:shipping_destinations])\n# where the controller/serializer first normalizes:\nnormalized = raw_destinations.index_by { |d| d['country_code'] }.values # last row per country wins\nlink.save_shipping_destinations!(normalized)","handlingStrategy":"validation","validationCode":"deduped = shipping_destinations.uniq { |d| d['country_code'] }\nraise ArgumentError, 'duplicate country' if deduped.size != shipping_destinations.size","typeGuard":null,"tryCatchPattern":"begin\n  link.save_shipping_destinations!(destinations)\nrescue Link::LinkInvalid\n  render json: { errors: link.errors.full_messages }, status: :unprocessable_entity\nend","preventionTips":["Disallow selecting an already-listed country in the shipping UI.","Replace-in-place (by country_code) rather than append when editing a destination row.","Guard against double form submission re-appending rows."],"tags":["links","shipping","unique-constraint","rails","validation","linkinvalid"],"backgroundTag":"duplicate-unique-constraint","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}