From 876e73a989f9c6be5c621f79ca1371ef60351ea2 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Thu, 13 Dec 2018 12:27:08 +1100 Subject: [PATCH] Rewrite task to generate sample data https://github.com/openfoodfoundation/openfoodnetwork/issues/2072 We need more and better structured sample data for dev testing and to bootstrap staging data. This new task aims to replace openfoodnetwork:dev:sample_data. --- lib/tasks/sample_data.rake | 654 +++++++++++++++++++++++++++++++++++++ 1 file changed, 654 insertions(+) create mode 100644 lib/tasks/sample_data.rake diff --git a/lib/tasks/sample_data.rake b/lib/tasks/sample_data.rake new file mode 100644 index 0000000000..d93300e51f --- /dev/null +++ b/lib/tasks/sample_data.rake @@ -0,0 +1,654 @@ +# The sample data generated by this task is supposed to save some time during +# manual testing. It is not meant to be complete, but we try to improve it +# over time. How much is hardcoded here is a trade off between developer time +# and tester time. We also can't include secrets like payment gateway +# configurations in the code since it's public. We have been discussing this for +# a while: +# +# - https://community.openfoodnetwork.org/t/seed-data-development-provisioning-deployment/910 +# - https://github.com/openfoodfoundation/openfoodnetwork/issues/2072 +# +namespace :openfoodnetwork do + desc 'load sample data for development or staging' + task sample_data: :environment do + raise "Please run `rake db:seed` first." unless seeded? + + users = UserFactory.new.create_samples + + enterprises = EnterpriseFactory.new.create_samples(users) + + PermissionFactory.new.create_samples(enterprises) + + FeeFactory.new.create_samples(enterprises) + + ShippingMethodFactory.new.create_samples(enterprises) + + PaymentMethodFactory.new.create_samples(enterprises) + + TaxonFactory.new.create_samples + + products = ProductFactory.new.create_samples(enterprises) + + InventoryFactory.new.create_samples(products) + + OrderCycleFactory.new.create_samples + + CustomerFactory.new.create_samples(users) + + GroupFactory.new.create_samples + end + + def seeded? + Spree::User.count > 0 && + Spree::Country.count > 0 && + Spree::State.count > 0 + end + + module Logging + private + + def log(message) + puts "[openfoodnetwork:sample_data:load] #{message}" + end + end + + module Addressing + private + + def address(string) + state = country.states.first + parts = string.split(", ") + Spree::Address.new( + address1: parts[0], + city: parts[1], + zipcode: parts[2], + state: state, + country: country + ) + end + + def zone + zone = Spree::Zone.find_or_create_by_name!("Australia") + zone.members.create(zonable: country) + zone + end + + def country + Spree::Country.find_by_iso(ENV.fetch('DEFAULT_COUNTRY_CODE')) + end + end + + class UserFactory + include Logging + + def create_samples + log "Creating users:" + usernames.map { |name| + create_user(name) + }.to_h + end + + private + + def usernames + [ + "Manel Super Admin", + "Penny Profile", + "Fred Farmer", + "Freddy Shop Farmer", + "Fredo Hub Farmer", + "Mary Retailer", + "Maryse Private", + "Jane Customer" + ] + end + + def create_user(name) + email = "#{name.downcase.tr(' ', '.')}@example.org" + password = Spree::User.friendly_token + log "- #{email}" + user = Spree::User.create_with( + password: password, + password_confirmation: password, + confirmation_sent_at: Time.zone.now, + confirmed_at: Time.zone.now + ).find_or_create_by_email!(email) + [name, user] + end + end + + class EnterpriseFactory + include Logging + include Addressing + + def create_samples(users) + log "Creating enterprises:" + enterprise_data(users).map do |data| + name = data[:name] + log "- #{name}" + Enterprise.create_with(data).find_or_create_by_name!(name) + end + end + + private + + # rubocop:disable Metrics/MethodLength + def enterprise_data(users) + [ + { + name: "Penny's Profile", + owner: users["Penny Profile"], + is_primary_producer: false, + sells: "none", + address: address("25 Myrtle Street, Bayswater, 3153") + }, + { + name: "Fred's Farm", + owner: users["Fred Farmer"], + is_primary_producer: true, + sells: "none", + address: address("6 Rollings Road, Upper Ferntree Gully, 3156") + }, + { + name: "Freddy's Farm Shop", + owner: users["Freddy Shop Farmer"], + is_primary_producer: true, + sells: "own", + address: address("72 Lake Road, Blackburn, 3130") + }, + { + name: "Fredo's Farm Hub", + owner: users["Fredo Hub Farmer"], + is_primary_producer: true, + sells: "any", + address: address("7 Verbena Street, Mordialloc, 3195") + }, + { + name: "Mary's Online Shop", + owner: users["Mary Retailer"], + is_primary_producer: false, + sells: "any", + address: address("20 Galvin Street, Altona, 3018") + }, + { + name: "Maryse's Private Shop", + owner: users["Maryse Private"], + is_primary_producer: false, + sells: "any", + address: address("6 Martin Street, Belgrave, 3160"), + require_login: true + } + ] + end + # rubocop:enable Metrics/MethodLength + end + + class PaymentMethodFactory + include Logging + include Addressing + + def create_samples(enterprises) + log "Creating payment methods:" + distributors = enterprises.select(&:is_distributor) + distributors.each do |enterprise| + create_payment_methods(enterprise) + end + end + + private + + def create_payment_methods(enterprise) + return if enterprise.payment_methods.present? + log "- #{enterprise.name}" + create_cash_method(enterprise) + create_card_method(enterprise) + end + + def create_cash_method(enterprise) + create_payment_method( + enterprise, + "Cash on collection", + "Pay on collection!", + Spree::Calculator::FlatRate.new + ) + end + + def create_card_method(enterprise) + create_payment_method( + enterprise, + "Credit card (fake)", + "We charge 1%, but won't ask for you details. ;-)", + Spree::Calculator::FlatPercentItemTotal.new(preferred_flat_percent: 1) + ) + end + + def create_payment_method(enterprise, name, description, calculator) + card = enterprise.payment_methods.new( + name: name, + description: description, + distributor_ids: [enterprise.id] + ) + card.calculator = calculator + card.save! + end + end + + class ShippingMethodFactory + include Logging + include Addressing + + def create_samples(enterprises) + log "Creating shipping methods:" + distributors = enterprises.select(&:is_distributor) + distributors.each do |enterprise| + create_shipping_methods(enterprise) + end + end + + private + + def create_shipping_methods(enterprise) + return if enterprise.shipping_methods.present? + log "- #{enterprise.name}" + create_pickup(enterprise) + create_delivery(enterprise) + end + + def create_pickup(enterprise) + create_shipping_method( + enterprise, + name: "Pickup", + description: "pick-up at your awesome hub gathering place", + require_ship_address: false, + calculator_type: "Calculator::Weight" + ) + end + + def create_delivery(enterprise) + delivery = create_shipping_method( + enterprise, + name: "Home delivery", + description: "yummy food delivered at your door", + require_ship_address: true, + calculator_type: "Spree::Calculator::FlatRate" + ) + delivery.calculator.preferred_amount = 2 + delivery.calculator.save! + end + + def create_shipping_method(enterprise, params) + params[:distributor_ids] = [enterprise.id] + method = enterprise.shipping_methods.new(params) + method.zone = zone + method.save! + method + end + end + + class FeeFactory + include Logging + + def create_samples(enterprises) + log "Creating fees:" + enterprises.each do |enterprise| + next if enterprise.enterprise_fees.present? + log "- #{enterprise.name} charges markup" + calculator = Calculator::FlatPercentPerItem.new(preferred_flat_percent: 10) + create_fee(enterprise, calculator) + calculator.save! + end + end + + private + + def create_fee(enterprise, calculator) + fee = enterprise.enterprise_fees.new( + fee_type: "sales", + name: "markup", + inherits_tax_category: true, + ) + fee.calculator = calculator + fee.save! + end + end + + class PermissionFactory + include Logging + + def create_samples(enterprises) + all_permissions = [ + :add_to_order_cycle, + :manage_products, + :edit_profile, + :create_variant_overrides + ] + enterprises.each do |enterprise| + log "#{enterprise.name} permits everybody to do everything." + enterprise_permits_to(enterprise, enterprises, all_permissions) + end + end + + private + + def enterprise_permits_to(enterprise, receivers, permissions) + receivers.each do |receiver| + EnterpriseRelationship.where( + parent_id: enterprise, + child_id: receiver + ).first_or_create!( + parent: enterprise, + child: receiver, + permissions_list: permissions + ) + end + end + end + + class TaxonFactory + include Logging + + def create_samples + log "Creating taxonomies:" + taxonomy = Spree::Taxonomy.find_or_create_by_name!('Products') + taxons = ['Vegetables', 'Fruit', 'Oils', 'Preserves and Sauces', 'Dairy', 'Meat and Fish'] + taxons.each do |taxon_name| + create_taxon(taxonomy, taxon_name) + end + end + + private + + def create_taxon(taxonomy, taxon_name) + return if Spree::Taxon.where(name: taxon_name).exists? + log "- #{taxon_name}" + Spree::Taxon.create!( + name: taxon_name, + parent_id: taxonomy.root.id, + taxonomy_id: taxonomy.id + ) + end + end + + class ProductFactory + include Logging + + def create_samples(enterprises) + log "Creating products:" + product_data(enterprises).map do |hash| + create_product(hash) + end + end + + private + + # rubocop:disable Metrics/MethodLength, Metrics/AbcSize + def product_data(enterprises) + vegetables = Spree::Taxon.find_by_name('Vegetables') + fruit = Spree::Taxon.find_by_name('Fruit') + meat = Spree::Taxon.find_by_name('Meat and Fish') + producers = enterprises.select(&:is_primary_producer) + distributors = enterprises.select(&:is_distributor) + [ + { + name: 'Garlic', + price: 20.00, + supplier: producers[0], + taxons: [vegetables], + distributor: distributors[0] + }, + { + name: 'Fuji Apple', + price: 5.00, + supplier: producers[1], + taxons: [fruit], + distributor: distributors[0] + }, + { + name: 'Beef - 5kg Trays', + price: 50.00, + supplier: producers[1], + taxons: [meat], + distributor: distributors[0] + }, + { + name: 'Carrots', + price: 3.00, + supplier: producers[2], + taxons: [vegetables], + distributor: distributors[0] + }, + { + name: 'Potatoes', + price: 2.00, + supplier: producers[2], + taxons: [vegetables], + distributor: distributors[0] + }, + { + name: 'Tomatoes', + price: 2.00, + supplier: producers[2], + taxons: [vegetables], + distributor: distributors[0] + } + ] + end + # rubocop:enable Metrics/MethodLength, Metrics/AbcSize + + def create_product(hash) + log "- #{hash[:name]}" + params = hash.merge( + supplier_id: hash[:supplier].id, + primary_taxon_id: hash[:taxons].first.id, + variant_unit: "weight", + variant_unit_scale: 1, + unit_value: 1, + on_demand: true + ) + create_product_with_distribution(params) + end + + def create_product_with_distribution(params) + product = Spree::Product.create_with(params).find_or_create_by_name(params[:name]) + ProductDistribution.create( + product: product, + distributor: params[:distributor] + ) + product + end + end + + class InventoryFactory + include Logging + + def create_samples(products) + log "Creating inventories" + marys_shop = Enterprise.find_by_name("Mary's Online Shop") + products.each do |product| + create_item(marys_shop, product) + end + end + + private + + def create_item(shop, product) + InventoryItem.create_with( + enterprise: shop, + variant: product.variants.first, + visible: true + ).find_or_create_by_variant_id(product.variants.first.id) + create_override(shop, product) + end + + def create_override(shop, product) + VariantOverride.create_with( + variant: product.variants.first, + hub: shop, + price: 12, + count_on_hand: 5 + ).find_or_create_by_variant_id(product.variants.first.id) + end + end + + class OrderCycleFactory + include Logging + # rubocop:disable Metrics/MethodLength + def create_samples + log "Creating order cycles" + create_order_cycle( + "Freddy's Farm Shop OC", + "Freddy's Farm Shop", + ["Freddy's Farm Shop"], + ["Freddy's Farm Shop"], + receival_instructions: "Dear self, don't forget the keys.", + pickup_time: "the weekend", + pickup_instructions: "Bring your own shopping bags or boxes." + ) + + create_order_cycle( + "Fredo's Farm Hub OC", + "Fredo's Farm Hub", + ["Fred's Farm", "Fredo's Farm Hub"], + ["Fredo's Farm Hub"], + receival_instructions: "Under the shed, please.", + pickup_time: "Wednesday 2pm", + pickup_instructions: "Boxes for packaging under the roof." + ) + + create_order_cycle( + "Mary's Online Shop OC", + "Mary's Online Shop", + ["Fred's Farm", "Freddy's Farm Shop", "Fredo's Farm Hub"], + ["Mary's Online Shop"], + receival_instructions: "Please shut the gate.", + pickup_time: "midday" + ) + + create_order_cycle( + "Multi Shop OC", + "Mary's Online Shop", + ["Fred's Farm", "Freddy's Farm Shop", "Fredo's Farm Hub"], + ["Mary's Online Shop", "Maryse's Private Shop"], + receival_instructions: "Please shut the gate.", + pickup_time: "dusk" + ) + end + # rubocop:enable Metrics/MethodLength + + private + + def create_order_cycle(name, coordinator_name, supplier_names, distributor_names, data) + coordinator = Enterprise.find_by_name(coordinator_name) + return if OrderCycle.active.where(name: name).exists? + + log "- #{name}" + cycle = create_order_cycle_with_fee(name, coordinator) + create_exchanges(cycle, supplier_names, distributor_names, data) + end + + def create_order_cycle_with_fee(name, coordinator) + cycle = OrderCycle.create!( + name: name, + orders_open_at: 1.day.ago, + orders_close_at: 1.month.from_now, + coordinator: coordinator + ) + cycle.coordinator_fees << coordinator.enterprise_fees.first + cycle + end + + def create_exchanges(cycle, supplier_names, distributor_names, data) + suppliers = Enterprise.where(name: supplier_names) + distributors = Enterprise.where(name: distributor_names) + + incoming = incoming_exchanges(cycle, suppliers, data) + outgoing = outgoing_exchanges(cycle, distributors, data) + all_exchanges = incoming + outgoing + add_products(suppliers, all_exchanges) + end + + def incoming_exchanges(cycle, suppliers, data) + suppliers.map do |supplier| + Exchange.create!( + order_cycle: cycle, + sender: supplier, + receiver: cycle.coordinator, + incoming: true, + receival_instructions: data[:receival_instructions] + ) + end + end + + def outgoing_exchanges(cycle, distributors, data) + distributors.map do |distributor| + Exchange.create!( + order_cycle: cycle, + sender: cycle.coordinator, + receiver: distributor, + incoming: false, + pickup_time: data[:pickup_time], + pickup_instructions: data[:pickup_instructions] + ) + end + end + + def add_products(suppliers, exchanges) + products = suppliers.flat_map(&:supplied_products) + products.each do |product| + exchanges.each { |exchange| exchange.variants << product.variants.first } + end + end + end + + class CustomerFactory + include Logging + + def create_samples(users) + log "Creating customers" + jane = users["Jane Customer"] + maryse_shop = Enterprise.find_by_name("Maryse's Private Shop") + return if Customer.where(user_id: jane, enterprise_id: maryse_shop).exists? + log "- #{jane.email}" + Customer.create!( + email: jane.email, + user: jane, + enterprise: maryse_shop + ) + end + end + + class GroupFactory + include Logging + include Addressing + + def create_samples + log "Creating groups" + return if EnterpriseGroup.where(name: "Producer group").exists? + + create_group( + name: "Producer group", + owner: enterprises.first.owner, + on_front_page: true, + description: "The seed producers", + address: "6 Rollings Road, Upper Ferntree Gully, 3156" + ) + end + + private + + def create_group(params) + group = EnterpriseGroup.new(params) + group.address = address(params[:address]) + group.enterprises = enterprises + group.save! + end + + def enterprises + @enterprises ||= Enterprise.where(name: [ + "Fred's Farm", + "Freddy's Farm Shop", + "Fredo's Farm Hub" + ]) + end + end +end