{"record":{"id":"b397a1376391d3a9","repo":"antiwork/gumroad","slug":"cannot-switch-to-per-variant-content-the-product","errorCode":null,"errorMessage":"Cannot switch to per-variant content: the product has no variants to migrate content to.","messagePattern":"Cannot switch to per-variant content: the product has no variants to migrate content to\\.","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":200,"severity":"error","filePath":"app/controllers/api/v2/links_controller.rb","lineNumber":1023,"sourceCode":"        @product.alive_rich_contents.each(&:mark_deleted!)\n      end\n    end\n\n    def clear_variant_rich_content!(retire_upsells: true)\n      @product.alive_variants.each do |variant|\n        retire_upsells_from_rich_contents!(variant.alive_rich_contents) if retire_upsells\n        variant.alive_rich_contents.each(&:mark_deleted!)\n        variant.product_files = []\n      end\n    end\n\n    def migrate_to_per_variant_rich_content!\n      product_pages = @product.alive_rich_contents.sort_by(&:position)\n      return if product_pages.empty?\n\n      variants = @product.alive_variants\n      if variants.empty?\n        raise Link::LinkInvalid, \"Cannot switch to per-variant content: the product has no variants to migrate content to.\"\n      end\n\n      destination_variant = variants.first\n      variants.each do |variant|\n        variant.alive_rich_contents.each(&:mark_deleted!)\n        variant.product_files = []\n      end\n\n      created = product_pages.each_with_index.map do |rc, index|\n        destination_variant.alive_rich_contents.create!(title: rc.title, description: rc.description_without_stale_dead_cross_product_file_embeds, position: index)\n      end\n      file_ids = created.flat_map { _1.embedded_product_file_ids_in_order }.uniq\n      destination_variant.product_files = file_ids.any? ? @product.product_files.alive.where(id: file_ids) : []\n\n      product_pages.each(&:mark_deleted!)\n    end\n\n    def canonicalize_rich_contents(entity)","sourceCodeStart":1005,"sourceCodeEnd":1041,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/controllers/api/v2/links_controller.rb#L1005-L1041","documentation":"Link::LinkInvalid raised by migrate_to_per_variant_rich_content! when has_same_rich_content_for_all_variants changes to false while the product has product-level alive rich contents but zero alive variants. Per-variant mode stores content on variants; with no variants there is no destination for the migration (destination_variant = variants.first would be nil), so the switch is rejected.","triggerScenarios":"PATCH /api/v2/links/:id setting has_same_rich_content_for_all_variants: false on a product that has alive product-level content and alive_variants.empty? — e.g. all variants were deleted but product pages remain.","commonSituations":"Sellers deleting every variant (product becomes single-tier) then toggling the content mode; API scripts flipping the flag before creating variants; products reduced to one SKU where variants were soft-deleted.","solutions":["Create at least one variant first, then switch the flag — the product-level pages migrate onto that variant.","Or clear product-level content first if you truly want per-variant mode with content added later per variant.","Order your API calls: create variants -> flip flag -> write variant content.","Check alive_variants (soft-deleted variants don't count) before attempting the switch."],"exampleFix":"# before: switch with no variants -> rejected\nLinkApi.update(link_id, has_same_rich_content_for_all_variants: false)\n# after: create the variant destination first\nLinkApi.create_variant(link_id, name: \"Standard\")\nLinkApi.update(link_id, has_same_rich_content_for_all_variants: false)","handlingStrategy":"validation","validationCode":"raise InvalidTransition, \"create a variant first\" if product.alive_variants.empty? && product.alive_rich_contents.any?","typeGuard":"def safe_to_switch_to_per_variant?(product)\n  product.alive_rich_contents.empty? || product.alive_variants.any?\nend","tryCatchPattern":null,"preventionTips":["Create the destination variant before flipping has_same_rich_content_for_all_variants to false.","Clear product-level content first if you want per-variant mode without migration.","Count only alive variants — soft-deleted ones don't provide a destination."],"tags":["api","rich-content","variants","migration","products","validation"],"backgroundTag":"invalid-state-transition","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}