{"record":{"id":"44052408a9c2c1f8","repo":"antiwork/gumroad","slug":"memberships-should-have-at-least-one-tier","errorCode":null,"errorMessage":"Memberships should have at least one tier.","messagePattern":"Memberships should have at least one tier\\.","errorType":"exception","errorClass":"Link::LinkInvalid","httpStatus":null,"severity":"error","filePath":"app/models/link.rb","lineNumber":1890,"sourceCode":"      if has_alive_categories_without_variants\n        errors.add(:base, \"Sorry, the product versions must have at least one option.\")\n      end\n    end\n\n    def valid_tier_version_structure\n      return if deleted_at.present?\n      return if archived?\n      return if purchase_disabled_at.present? && purchase_disabled_at_changed?\n\n      if variant_categories.alive.size != 1\n        errors.add(:base, \"Memberships should only have one Tier version category.\")\n        raise LinkInvalid, \"Memberships should only have one Tier version category.\"\n        return\n      end\n\n      if variant_categories.alive.first.variants.alive.size == 0\n        errors.add(:base, \"Memberships should have at least one tier.\")\n        raise LinkInvalid, \"Memberships should have at least one tier.\"\n      end\n    end\n\n    def quantity_enabled_state_is_allowed\n      if quantity_enabled && !can_enable_quantity?\n        errors.add(:base, \"Customers cannot be allowed to choose a quantity for this product.\")\n      end\n    end\n\n    def custom_permalink_or_is_licensed_changed?\n      custom_permalink_changed? || is_licensed_changed?\n    end\n\n    def description_scrubber\n      unless Loofah::HTML5::SafeList::ACCEPTABLE_CSS_PROPERTIES.include?(\"position\")\n        # TODO (vishal): iframe.ly adds \"position\" style, which is not allowed by default.\n        # We should be able to remove this once https://github.com/flavorjones/loofah/pull/258 is merged.\n        Loofah::HTML5::SafeList::ACCEPTABLE_CSS_PROPERTIES.add(\"position\")","sourceCodeStart":1872,"sourceCodeEnd":1908,"githubUrl":"https://github.com/antiwork/gumroad/blob/afeacbd394069a1cbf0c6c50ee8e900925050370/app/models/link.rb#L1872-L1908","documentation":"Raised as Link::LinkInvalid by Link#valid_tier_version_structure when a membership's single alive variant category contains zero alive variants. The membership must sell at least one tier, so an empty Tier category aborts the save. Same skip conditions as its sibling check: deleted, archived, or purchase_disabled_at-changed links are exempt.","triggerScenarios":"Saving an alive membership link where variant_categories.alive.first.variants.alive is empty — seller deleted every tier row before saving, or an import created the category but no variants, or all variants were soft-deleted.","commonSituations":"Tier editor grid allowing the last row to be removed; imports partial-failing after category creation; cleanup jobs soft-deleting variants without checking membership integrity.","solutions":["Keep at least one alive tier variant on the Tier category — block removal of the last row in the UI.","For imports, create category and at least one variant in the same transaction.","Rescue Link::LinkInvalid and show errors.full_messages."],"exampleFix":"# before\nlink.save! # LinkInvalid: zero alive tiers\n\n# after\nif link.variant_categories.alive.first.variants.alive.empty?\n  return { error: \"Memberships should have at least one tier.\" }\nend\nlink.save!","handlingStrategy":"validation","validationCode":"return { error: 'At least one tier required.' } if link.is_membership && link.variant_categories.alive.first&.variants&.alive&.empty?","typeGuard":null,"tryCatchPattern":"begin\n  link.save!\nrescue Link::LinkInvalid\n  render json: { errors: link.errors.full_messages }, status: :unprocessable_entity\nend","preventionTips":["Prevent deleting the last alive tier variant in the tier editor.","Create category + at least one variant atomically during imports.","Check variants.alive, not all variants — soft-deleted tiers don't count."],"tags":["links","memberships","variants","tiers","rails","linkinvalid","validation"],"backgroundTag":"at-least-one-variant-required","analyzedSha":"afeacbd394069a1cbf0c6c50ee8e900925050370","analyzedAt":"2026-08-21T17:58:52.159Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}