From 7c5fa2caa5ded0359bef29730601a5d65af8eb42 Mon Sep 17 00:00:00 2001 From: Nat Budin Date: Mon, 22 Jun 2026 10:13:47 -0700 Subject: [PATCH 1/2] Extend ImportConventionDataService with CMS layouts and store item pricing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds support for importing Eventlite-exported conventions, which include CMS layout records and ticket-providing products with pricing data. - Add import_cms_layouts: creates CmsLayout records and returns a name→layout map - Update import_cms_content to call import_cms_layouts first and pass the map to import_cms_pages - Update import_cms_pages to assign cms_layout from the map when cms_layout_name is present in the export data - Update import_store_items to set pricing_structure (fixed price), payment_options (["stripe"] when price > 0), and provides_ticket_type from the new schema fields Also fixes a latent bug: products.pricing_structure is NOT NULL but the previous import_store_items code never set it, so any import with store_items would fail at the DB level. Co-Authored-By: Claude Sonnet 4.6 --- .../import_convention_data_service.rb | 27 ++++- .../import_convention_data_service_test.rb | 107 ++++++++++++++++++ 2 files changed, 131 insertions(+), 3 deletions(-) diff --git a/app/services/import_convention_data_service.rb b/app/services/import_convention_data_service.rb index de77e1fba5f..0fd518fedc7 100644 --- a/app/services/import_convention_data_service.rb +++ b/app/services/import_convention_data_service.rb @@ -100,12 +100,22 @@ def load_cms_content_set(convention) end def import_cms_content(convention) + layout_map = import_cms_layouts(convention) import_cms_files(convention) - import_cms_pages(convention) + import_cms_pages(convention, layout_map) import_cms_partials(convention) import_cms_navigation_items(convention) end + def import_cms_layouts(convention) + (data[:cms_layouts] || []).each_with_object({}) do |layout_data, map| + layout = convention.cms_layouts.find_or_initialize_by(name: layout_data[:name]) + layout.content = layout_data[:content] + layout.save! + map[layout_data[:name]] = layout + end + end + def import_cms_files(convention) existing = Set.new(convention.cms_files.joins(file_attachment: :blob).pluck("lower(active_storage_blobs.filename)")) @@ -122,10 +132,11 @@ def import_cms_files(convention) end end - def import_cms_pages(convention) + def import_cms_pages(convention, layout_map = {}) (data[:cms_pages] || []).each do |page_data| page = convention.pages.find_or_initialize_by(slug: page_data[:slug]) page.assign_attributes(name: page_data[:name], content: page_data[:content]) + page.cms_layout = layout_map[page_data[:cms_layout_name]] if page_data[:cms_layout_name].present? page.save! end end @@ -409,12 +420,22 @@ def import_store(convention, user_con_profile_map) end def import_store_items(convention) + ticket_type_map = convention.ticket_types.index_by(&:name) + (data[:store_items] || []).each_with_object({}) do |item, map| + price_money = item[:price] ? money_value(item[:price]) : Money.new(0, Money.default_currency) + pricing_structure = PricingStructure.new(pricing_strategy: "fixed", value: price_money) + payment_options = price_money.positive? ? ["stripe"] : [] + product = convention.products.create!( name: item[:name], description: item[:description], - available: item.key?(:available) ? item[:available] : false + available: item.key?(:available) ? item[:available] : false, + pricing_structure: pricing_structure, + payment_options: payment_options, + provides_ticket_type: + item[:provides_ticket_type_name] ? ticket_type_map[item[:provides_ticket_type_name]] : nil ) map[item[:name]] = product end diff --git a/test/services/import_convention_data_service_test.rb b/test/services/import_convention_data_service_test.rb index 2ceba198963..61e91138560 100644 --- a/test/services/import_convention_data_service_test.rb +++ b/test/services/import_convention_data_service_test.rb @@ -613,5 +613,112 @@ class ImportConventionDataServiceTest < ActiveSupport::TestCase assert_equal org, convention.organization end end + + describe "CMS layouts" do + before do + data = + base_data.merge( + cms_layouts: [ + { name: "Default Layout", content: "{{ content_for_layout }}" }, + { name: "Minimal Layout", content: "{{ content_for_layout }}" } + ], + cms_pages: [ + { name: "Home", slug: "home", content: "Welcome", cms_layout_name: "Default Layout" }, + { name: "About", slug: "about", content: "About us" } + ] + ) + ImportConventionDataService.new(data: data).call! + end + + it "creates layouts" do + convention = Convention.find_by!(domain: "importtest.example.com") + assert convention.cms_layouts.find_by(name: "Default Layout") + assert convention.cms_layouts.find_by(name: "Minimal Layout") + end + + it "sets layout content" do + convention = Convention.find_by!(domain: "importtest.example.com") + layout = convention.cms_layouts.find_by!(name: "Default Layout") + assert_equal "{{ content_for_layout }}", layout.content + end + + it "assigns cms_layout to a page when cms_layout_name is present" do + convention = Convention.find_by!(domain: "importtest.example.com") + page = convention.pages.find_by!(slug: "home") + assert_equal "Default Layout", page.cms_layout.name + end + + it "leaves cms_layout nil when cms_layout_name is absent" do + convention = Convention.find_by!(domain: "importtest.example.com") + page = convention.pages.find_by!(slug: "about") + assert_nil page.cms_layout + end + end + + describe "store items with pricing" do + before do + data = + base_data.merge( + convention: + base_data[:convention].merge( + ticket_mode: "required_for_signup", + ticket_types: [ + { name: "weekend_pass", allows_event_signups: true, counts_towards_convention_maximum: true } + ] + ), + store_items: [ + { + name: "Weekend Pass", + available: true, + price: { + fractional: 5000, + currency_code: "USD" + }, + provides_ticket_type_name: "weekend_pass" + }, + { name: "Free Volunteer Pass", available: true, provides_ticket_type_name: "weekend_pass" } + ] + ) + ImportConventionDataService.new(data: data).call! + end + + it "creates products" do + convention = Convention.find_by!(domain: "importtest.example.com") + assert convention.products.find_by(name: "Weekend Pass") + assert convention.products.find_by(name: "Free Volunteer Pass") + end + + it "sets pricing_structure to fixed with the given price" do + convention = Convention.find_by!(domain: "importtest.example.com") + product = convention.products.find_by!(name: "Weekend Pass") + assert_equal "fixed", product.pricing_structure.pricing_strategy + assert_equal Money.new(5000, "USD"), product.pricing_structure.value + end + + it "sets pricing_structure to a zero fixed price when no price is given" do + convention = Convention.find_by!(domain: "importtest.example.com") + product = convention.products.find_by!(name: "Free Volunteer Pass") + assert_equal "fixed", product.pricing_structure.pricing_strategy + assert_equal Money.new(0, Money.default_currency), product.pricing_structure.value + end + + it "sets payment_options to stripe when price is positive" do + convention = Convention.find_by!(domain: "importtest.example.com") + product = convention.products.find_by!(name: "Weekend Pass") + assert_equal ["stripe"], product.payment_options + end + + it "sets payment_options to empty when price is zero" do + convention = Convention.find_by!(domain: "importtest.example.com") + product = convention.products.find_by!(name: "Free Volunteer Pass") + assert_equal [], product.payment_options + end + + it "links provides_ticket_type to the named ticket type" do + convention = Convention.find_by!(domain: "importtest.example.com") + product = convention.products.find_by!(name: "Weekend Pass") + assert_equal "weekend_pass", product.provides_ticket_type.name + end + end end # rubocop:enable Metrics/ClassLength, Metrics/BlockLength From baf8ba55b4e4dbcd3802fd5afeb3e62ee9831a53 Mon Sep 17 00:00:00 2001 From: Nat Budin Date: Mon, 22 Jun 2026 11:59:52 -0700 Subject: [PATCH 2/2] Wire up convention default_layout and root_page from import data After importing CMS layouts and pages, check for default_layout_name and root_page_slug in the convention data and set the associations on the convention record. Co-Authored-By: Claude Sonnet 4.6 --- app/services/import_convention_data_service.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/services/import_convention_data_service.rb b/app/services/import_convention_data_service.rb index 0fd518fedc7..b28b0002073 100644 --- a/app/services/import_convention_data_service.rb +++ b/app/services/import_convention_data_service.rb @@ -105,6 +105,12 @@ def import_cms_content(convention) import_cms_pages(convention, layout_map) import_cms_partials(convention) import_cms_navigation_items(convention) + + con = data[:convention] + updates = {} + updates[:default_layout] = layout_map[con[:default_layout_name]] if con[:default_layout_name].present? + updates[:root_page] = convention.pages.find_by(slug: con[:root_page_slug]) if con[:root_page_slug].present? + convention.update!(updates) if updates.any? end def import_cms_layouts(convention)