From f986c5898e5d0603a5166dd8dc21b30f13681226 Mon Sep 17 00:00:00 2001 From: Lynne Date: Sat, 16 Apr 2016 03:23:26 +0100 Subject: [PATCH 01/27] Updating en-GB file to reflect recent additions (#909) --- config/locales/en-GB.yml | 458 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 427 insertions(+), 31 deletions(-) diff --git a/config/locales/en-GB.yml b/config/locales/en-GB.yml index 931df49417..06039d9d66 100644 --- a/config/locales/en-GB.yml +++ b/config/locales/en-GB.yml @@ -21,18 +21,129 @@ en-GB: invalid: | Invalid email or password. Were you a guest last time? Perhaps you need to create an account or reset your password. + enterprise_confirmations: + enterprise: + confirmed: Thankyou, your email address has been confirmed. + not_confirmed: Your email address could not be confirmed. Perhaps you have already completed this step? + confirmation_sent: "Confirmation email sent!" + confirmation_not_sent: "Could not send a confirmation email." home: "OFN" title: Open Food Network welcome_to: 'Welcome to ' + site_meta_description: "We begin from the ground up. With farmers and growers ready to tell their stories proudly and truly. With distributors ready to connect people with products fairly and honestly. With buyers who believe that better weekly shopping decisions can…" search_by_name: Search by name... producers: UK Producers producers_join: UK producers are now welcome to join Open Food Network UK. - charges_sales_tax: Charges sales tax? - print: "Print" + charges_sales_tax: Charges VAT? + print_invoice: "Print Invoice" + send_invoice: "Send Invoice" + resend_confirmation: "Resend Confirmation" + view_order: "View Order" + edit_order: "Edit Order" + ship_order: "Ship Order" + cancel_order: "Cancel Order" + confirm_send_invoice: "An invoice for this order will be sent to the customer. Are you sure you want to continue?" + confirm_resend_order_confirmation: "Are you sure you want to resend the order confirmation email?" + invoice: "Invoice" + percentage_of_sales: "%{percentage} of sales" + percentage_of_turnover: "Percentage of turnover" + monthly_cap_excl_tax: "monthly cap (excl. VAT)" + capped_at_cap: "capped at %{cap}" + per_month: "per month" + free: "free" + plus_tax: "plus GST" + total_monthly_bill_incl_tax: "Total Monthly Bill (Incl. Tax)" + say_no: "No" + say_yes: "Yes" - logo: "Logo (640x130)" - logo_mobile: "Mobile logo (75x26)" - logo_mobile_svg: "Mobile logo (SVG)" + sort_order_cycles_on_shopfront_by: "Sort Order Cycles On Shopfront By" + + + admin: + # General form elements + quick_search: Quick Search + clear_all: Clear All + producer: Producer + shop: Shop + product: Product + variant: Variant + + columns: Columns + actions: Actions + viewing: "Viewing: %{current_view_name}" + + whats_this: What's this? + + customers: + index: + add_customer: "Add customer" + customer_placeholder: "customer@example.org" + inventory: + title: Inventory + description: Use this page to manage inventories for your enterprises. Any product details set here will override those set on the 'Products' page + sku: SKU + price: Price + on_hand: On Hand + on_demand: On Demand? + enable_reset: Enable Stock Level Reset? + inherit: Inherit? + add: Add + hide: Hide + select_a_shop: Select A Shop + review_now: Review Now + new_products_alert_message: There are %{new_product_count} new products available to add to your inventory. + currently_empty: Your inventory is currently empty + no_matching_products: No matching products found in your inventory + no_hidden_products: No products have been hidden from this inventory + no_matching_hidden_products: No hidden products match your search criteria + no_new_products: No new products are available to add to this inventory + no_matching_new_products: No new products match your search criteria + inventory_powertip: This is your inventory of products. To add products to your inventory, select 'New Products' from the Viewing dropdown. + hidden_powertip: These products have been hidden from your inventory and will not be available to add to your shop. You can click 'Add' to add a product to you inventory. + new_powertip: These products are available to be added to your inventory. Click 'Add' to add a product to your inventory, or 'Hide' to hide it from view. You can always change your mind later! + + + order_cycle: + choose_products_from: "Choose Products From:" + + enterprise: + select_outgoing_oc_products_from: Select outgoing OC products from + + enterprises: + form: + primary_details: + shopfront_requires_login: "Shopfront requires login?" + shopfront_requires_login_tip: "Choose whether customers must login to view the shopfront." + shopfront_requires_login_false: "Public" + shopfront_requires_login_true: "Require customers to login" + + home: + hubs: + show_closed_shops: "Show closed shops" + hide_closed_shops: "Hide closed shops" + show_on_map: "Show all on the map" + shared: + register_call: + selling_on_ofn: "Interested in getting on the Open Food Network?" + register: "Register here" + shop: + messages: + login: "login" + register: "register" + contact: "contact" + require_customer_login: "This shop is for customers only." + require_login_html: "Please %{login} if you have an account already. Otherwise, %{register} to become a customer." + require_customer_html: "Please %{contact} %{enterprise} to become a customer." + + # Printable Invoice Columns + invoice_column_item: "Item" + invoice_column_qty: "Qty" + invoice_column_tax: "VAT" + invoice_column_price: "Price" + + logo: "Logo (640x130)" #FIXME + logo_mobile: "Mobile logo (75x26)" #FIXME + logo_mobile_svg: "Mobile logo (SVG)" #FIXME home_hero: "Hero image" home_show_stats: "Show statistics" footer_logo: "Logo (220x76)" @@ -46,11 +157,10 @@ en-GB: footer_links_md: "Links" footer_about_url: "About URL" footer_tos_url: "Terms of Service URL" - invoice: "Invoice" name: Name - first_name: First name - last_name: Last name + first_name: First Name + last_name: Last Name email: Email phone: Phone next: Next @@ -90,9 +200,9 @@ en-GB: cart_empty: "Cart empty" cart_edit: "Edit your cart" - card_number: Card number - card_securitycode: "Security code" - card_expiry_date: Expiry date + card_number: Card Number + card_securitycode: "Security Code" + card_expiry_date: Expiry Date ofn_cart_headline: "Current cart for:" ofn_cart_distributor: "Distributor:" @@ -187,7 +297,7 @@ en-GB: checkout_cart_total: Cart total checkout_shipping_price: Shipping checkout_total_price: Total - checkout_back_to_cart: "Back to cart" + checkout_back_to_cart: "Back to Cart" order_paid: PAID order_not_paid: NOT PAID @@ -197,12 +307,32 @@ en-GB: order_delivery_on: Delivery on order_delivery_address: Delivery address order_special_instructions: "Your notes:" - order_pickup_instructions: Collection instructions + order_pickup_time: Ready for collection + order_pickup_instructions: Collection Instructions order_produce: Produce order_total_price: Total order_includes_tax: (includes tax) order_payment_paypal_successful: Your payment via PayPal has been processed successfully. - order_hub_info: Hub info + order_hub_info: Hub Info + + bom_tip: "Use this page to alter product quantities across multiple orders. Products may also be removed from orders entirely, if required." + bom_shared: "Shared Resource?" + bom_page_title: "Bulk Order Management" + bom_no: "Order no." + bom_date: "Order date" + bom_cycle: "Order cycle" + bom_max: "Max" + bom_hub: "Hub" + bom_variant: "Product: Unit" + bom_final_weigth_volume: "Weight/Volume" + bom_quantity: "Quantity" + bom_actions_delete: "Delete Selected" + bom_loading: "Loading orders" + bom_no_results: "No orders found." + bom_order_error: "Some errors must be resolved before you can update orders.\nAny fields with red borders contain errors." + + unsaved_changes_warning: "Unsaved changes exist and will be lost if you continue." + unsaved_changes_error: "Fields with red borders contain errors." products: "Products" products_in: "in %{oc}" @@ -307,6 +437,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using products_cart_empty: "Cart empty" products_edit_cart: "Edit your cart" products_from: from + products_change: "No changes to save." + products_update_error: "Saving failed with the following error(s):" + products_update_error_msg: "Saving failed." + products_update_error_data: "Save failed due to invalid data:" + products_changes_saved: "Changes saved." search_no_results_html: "Sorry, no results found for %{query}. Try another search?" @@ -317,9 +452,11 @@ See the %{link} to find out more about %{sitename}'s features and to start using groups_title: Groups groups_headline: Groups / regions + groups_text: "Every producer is unique. Every business has something different to offer. Our groups are collectives of producers, hubs and distributors who share something in common like location, farmers market or philosophy. This makes your shopping experience easier. So explore our groups and have the curating done for you." groups_search: "Search name or keyword" groups_no_groups: "No groups found" groups_about: "About Us" + groups_producers: "Our producers" groups_hubs: "Our hubs" groups_contact_web: Contact @@ -384,9 +521,9 @@ See the %{link} to find out more about %{sitename}'s features and to start using ocs_close_time: "ORDERS CLOSE" ocs_when_headline: When do you want your order? ocs_when_text: No products are displayed until you select a date. - ocs_when_closing: "Closing on" + ocs_when_closing: "Closing On" ocs_when_choose: "Choose Order Cycle" - ocs_list: "List view" + ocs_list: "List View" producers_about: About us producers_buy: Shop for @@ -407,13 +544,26 @@ See the %{link} to find out more about %{sitename}'s features and to start using producers_signup_cta_headline: Join now! producers_signup_cta_action: Join now producers_signup_detail: Here's the detail. + producer: Producer products_item: Item products_description: Description products_variant: Variant products_quantity: Quantity products_availabel: Available? - products_price: Price + products_producer: "Producer" + products_price: "Price" + products_sku: "SKU" + products_name: "name" + products_unit: "unit" + products_on_hand: "on hand" + products_on_demand: "On demand?" + products_category: "Category" + products_tax_category: "tax category" + products_available_on: "Available On" + products_inherit: "Inherit?" + products_inherits_properties: "Inherits Properties?" + products_stock_level_reset: "Enable Stock Level Reset?" register_title: Register @@ -431,7 +581,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using shops_signup_detail: Here's the detail. orders_fees: Fees... - orders_edit_title: Shopping cart + orders_edit_title: Shopping Cart orders_edit_headline: Your shopping cart orders_edit_time: Order ready for orders_edit_continue: Continue shopping @@ -509,18 +659,17 @@ See the %{link} to find out more about %{sitename}'s features and to start using confirm_password: "Confirm password" action_signup: "Sign up now" welcome_to_ofn: "Welcome to the Open Food Network!" - signup_or_login: "Start By signing up (or logging in)" + signup_or_login: "Start By Signing Up (or logging in)" have_an_account: "Already have an account?" action_login: "Log in now." - forgot_password: "Forgot password?" + forgot_password: "Forgot Password?" password_reset_sent: "An email with instructions on resetting your password has been sent!" reset_password: "Reset password" registration_greeting: "Greetings!" who_is_managing_enterprise: "Who is responsible for managing %{enterprise}?" - enterprise_contact: "Primary contact" + enterprise_contact: "Primary Contact" enterprise_contact_required: "You need to enter a primary contact." - enterprise_email: "Email address" - enterprise_email_required: "You need to enter valid email address." + enterprise_email_address: "Email address" enterprise_phone: "Phone number" back: "Back" continue: "Continue" @@ -528,20 +677,20 @@ See the %{link} to find out more about %{sitename}'s features and to start using limit_reached_message: "You have reached the limit!" limit_reached_text: "You have reached the limit for the number of enterprises you are allowed to own on the" limit_reached_action: "Return to the homepage" - select_promo_image: "Step 3. Select promo image" + select_promo_image: "Step 3. Select Promo Image" promo_image_tip: "Tip: Shown as a banner, preferred size is 1200×260px" promo_image_label: "Choose a promo image" action_or: "OR" promo_image_drag: "Drag and drop your promo here" - review_promo_image: "Step 4. Review your promo banner" + review_promo_image: "Step 4. Review Your Promo Banner" review_promo_image_tip: "Tip: for best results, your promo image should fill the available space" promo_image_placeholder: "Your logo will appear here for review once uploaded" uploading: "Uploading..." - select_logo: "Step 1. Select logo image" + select_logo: "Step 1. Select Logo Image" logo_tip: "Tip: Square images will work best, preferably at least 300×300px" logo_label: "Choose a logo image" logo_drag: "Drag and drop your logo here" - review_logo: "Step 2. Review your logo" + review_logo: "Step 2. Review Your Logo" review_logo_tip: "Tip: for best results, your logo should fill the available space" logo_placeholder: "Your logo will appear here for review once uploaded" enterprise_about_headline: "Nice one!" @@ -598,14 +747,14 @@ Please follow the instructions there to make your enterprise visible on the Open registration_type_error: "Please choose one. Are you are producer?" registration_type_producer_help: "Producers make yummy things to eat and/or drink. You're a producer if you grow it, raise it, brew it, bake it, ferment it, milk it or mould it." registration_type_no_producer_help: "If you’re not a producer, you’re probably someone who sells and distributes food. You might be a hub, coop, buying group, retailer, wholesaler or other." - create_profile: "Create profile" + create_profile: "Create Profile" registration_images_headline: "Thanks!" registration_images_description: "Let's upload some pretty pictures so your profile looks great! :)" - registration_detail_headline: "Let's get started" + registration_detail_headline: "Let's Get Started" registration_detail_enterprise: "Woot! First we need to know a little bit about your enterprise:" registration_detail_producer: "Woot! First we need to know a little bit about your farm:" - registration_detail_name_enterprise: "Enterprise name:" - registration_detail_name_producer: "Farm name:" + registration_detail_name_enterprise: "Enterprise Name:" + registration_detail_name_producer: "Farm Name:" registration_detail_name_placeholder: "e.g. Charlie's Awesome Farm" registration_detail_name_error: "Please choose a unique name for your enterprise" registration_detail_address1: "Address line 1:" @@ -641,3 +790,250 @@ Please follow the instructions there to make your enterprise visible on the Open price_graph: "Price graph" included_tax: "Included tax" remove_tax: "Remove tax" + balance: "Balance" + transaction: "Transaction" + transaction_date: "Date" #Transaction is only in key to avoid conflict with :date + payment_state: "Payment status" + shipping_state: "Shipping status" + value: "Value" + balance_due: "Balance due" + credit: "Credit" + Paid: "Paid" + Ready: "Ready" + you_have_no_orders_yet: "You have no orders yet" + running_balance: "Running balance" + outstanding_balance: "Outstanding balance" + admin_entreprise_relationships: "Enterprise Relationships" + admin_entreprise_relationships_everything: "Everything" + admin_entreprise_relationships_permits: "permits" + admin_entreprise_relationships_seach_placeholder: "Search" + admin_entreprise_relationships_button_create: "Create" + admin_entreprise_groups: "Enterprise Groups" + admin_entreprise_groups_name: "Name" + admin_entreprise_groups_owner: "Owner" + admin_entreprise_groups_on_front_page: "On front page ?" + admin_entreprise_groups_entreprise: "Enterprises" + admin_entreprise_groups_primary_details: "Primary Details" + admin_entreprise_groups_data_powertip: "The primary user responsible for this group." + admin_entreprise_groups_data_powertip_logo: "This is the logo for the group" + admin_entreprise_groups_data_powertip_promo_image: "This image is displayed at the top of the Group profile" + admin_entreprise_groups_about: "About" + admin_entreprise_groups_images: "Images" + admin_entreprise_groups_contact: "Contact" + admin_entreprise_groups_contact_phone_placeholder: "eg. 98 7654 3210" + admin_entreprise_groups_contact_address1_placeholder: "eg. 123 High Street" + admin_entreprise_groups_contact_city: "Suburb" + admin_entreprise_groups_contact_city_placeholder: "eg. Northcote" + admin_entreprise_groups_contact_zipcode: "Postcode" + admin_entreprise_groups_contact_zipcode_placeholder: "eg. 3070" + admin_entreprise_groups_contact_state_id: "State" + admin_entreprise_groups_contact_country_id: "Country" + admin_entreprise_groups_web: "Web Resources" + admin_entreprise_groups_web_twitter: "eg. @the_prof" + admin_entreprise_groups_web_website_placeholder: "eg. www.truffles.com" + admin_order_cycles: "Admin Order Cycles" + open: "Open" + close: "Close" + supplier: "Supplier" + coordinator: "Coordinator" + distributor: "Distributor" + product: "Products" + enterprise_fees: "Enterprise Fees" + fee_type: "Fee Type" + tax_category: "Tax Category" + calculator: "Calculator" + calculator_values: "Calculator values" + new_order_cycles: "New Order Cycles" + select_a_coordinator_for_your_order_cycle: "select a coordinator for your order cycle" + edit_order_cycle: "Edit Order Cycle" + roles: "Roles" + update: "Update" + add_producer_property: "Add producer property" + admin_settings: "Settings" + update_invoice: "Update Invoices" + finalise_invoice: "Finalise Invoices" + finalise_user_invoices: "Finalise User Invoices" + finalise_user_invoice_explained: "Use this button to finalize all invoices in the system for the previous calendar month. This task can be set up to run automatically once a month." + manually_run_task: "Manually Run Task " + update_user_invoices: "Update User Invoices" + update_user_invoice_explained: "Use this button to immediately update invoices for the month to date for each enterprise user in the system. This task can be set up to run automatically every night." + auto_finalise_invoices: "Auto-finalise invoices monthly on the 2nd at 1:30am" + auto_update_invoices: "Auto-update invoices nightly at 1:00am" + in_progress: "In Progress" + started_at: "Started at" + queued: "Queued" + scheduled_for: "Scheduled for" + customers: "Customers" + please_select_hub: "Please select a Hub" + loading_customers: "Loading Customers" + no_customers_found: "No customers found" + go: "Go" + hub: "Hub" + accounts_administration_distributor: "accounts administration distributor" + accounts_and_billing: "Accounts & Billing" + producer: "Producer" + product: "Product" + price: "Price" + on_hand: "On hand" + save_changes: "Save Changes" + spree_admin_overview_enterprises_header: "My Enterprises" + spree_admin_overview_enterprises_footer: "MANAGE MY ENTERPRISES" + spree_admin_enterprises_hubs_name: "Name" + spree_admin_enterprises_create_new: "CREATE NEW" + spree_admin_enterprises_shipping_methods: "Shipping Methods" + spree_admin_enterprises_fees: "Enterprise Fees" + spree_admin_enterprises_none_create_a_new_enterprise: "CREATE A NEW ENTERPRISE" + spree_admin_enterprises_none_text: "You don't have any enterprises yet" + spree_admin_enterprises_producers_name: "Name" + spree_admin_enterprises_producers_total_products: "Total Products" + spree_admin_enterprises_producers_active_products: "Active Products" + spree_admin_enterprises_producers_order_cycles: "Products in OCs" + spree_admin_enterprises_producers_order_cycles_title: "" + spree_admin_enterprises_tabs_hubs: "HUBS" + spree_admin_enterprises_tabs_producers: "PRODUCERS" + spree_admin_enterprises_producers_manage_order_cycles: "MANAGE ORDER CYCLES" + spree_admin_enterprises_producers_manage_products: "MANAGE PRODUCTS" + spree_admin_enterprises_producers_orders_cycle_text: "You don't have any active order cycles." + spree_admin_enterprises_any_active_products_text: "You don't have any active products." + spree_admin_enterprises_create_new_product: "CREATE A NEW PRODUCT" + spree_admin_order_cycles: "Order Cycles" + spree_admin_order_cycles_tip: "Order cycles determine when and where your products are available to customers." + dashbord: "Dashboard" + spree_admin_single_enterprise_alert_mail_confirmation: "Please confirm the email address for" + spree_admin_single_enterprise_alert_mail_sent: "We've sent an email to" + spree_admin_overview_action_required: "Action Required" + spree_admin_overview_check_your_inbox: "Please check you inbox for furher instructions. Thanks!" + change_package: "Change Package" + spree_admin_single_enterprise_hint: "Hint: To allow people to find you, turn on your visibility under" + your_profil_live: "Your profile live" + on_ofn_map: "on the Open Food Network map" + see: "See" + live: "live" + manage: "Manage" + resend: "Resend" + add_and_manage_products: "Add & manage products" + add_and_manage_order_cycles: "Add & manage order cycles" + manage_order_cycles: "Manage order cycles" + manage_products: "Manage products" + edit_profile_details: "Edit profile details" + edit_profile_details_etc: "Change your profile description, images, etc." + start_date: "Start Date" + end_date: "End Date" + order_cycle: "Order Cycle" + group_buy_unit_size: "Group Buy Unit Size" + total_qtt_ordered: "Total Quantity Ordered" + max_qtt_ordered: "Max Quantity Ordered" + current_fulfilled_units: "Current Fulfilled Units" + max_fulfilled_units: "Max Fulfilled Units" + bulk_management_warning: "WARNING: Some variants do not have a unit value" + ask: "Ask?" + no_orders_found: "No orders found." + order_no: "Order No." + weight_volume: "Weight/Volume" + remove_tax: "Remove tax" + tax_settings: "Tax Settings" + products_require_tax_category: "products require tax category" + admin_shared_address_1: "Address" + admin_shared_address_2: "Address (cont.)" + admin_share_city: "City" + admin_share_zipcode: "Postcode" + admin_share_country: "Country" + admin_share_state: "State" + hub_sidebar_hubs: "Hubs" + hub_sidebar_none_available: "None Available" + hub_sidebar_manage: "Manage" + hub_sidebar_at_least: "At least one hub must be selected" + hub_sidebar_blue: "blue" + hub_sidebar_red: "red" + shop_trial_in_progress: "Your shopfront trial expires in %{days}." + shop_trial_expired: "Good news! We have decided to extend shopfront trials until further notice (probably around March 2015)." #FIXME + report_customers_distributor: "Distributor" + report_customers_supplier: "Supplier" + report_customers_cycle: "Order Cycle" + report_customers_type: "Report Type" + report_customers_csv: "Download as csv" + report_producers: "Producers: " + report_type: "Report Type: " + report_hubs: "Hubs: " + report_payment: "Payment Methods: " + report_distributor: "Distributor: " + report_payment_by: 'Payments By Type' + report_itemised_payment: 'Itemised Payment Totals' + report_payment_totals: 'Payment Totals' + report_all: 'all' + report_order_cycle: "Order Cycle: " + report_entreprises: "Enterprises: " + report_users: "Users: " + initial_invoice_number: "Initial invoice number:" + invoice_date: "Invoice date:" + due_date: "Due date:" + account_code: "Account code:" + equals: "Equals" + contains: "contains" + discount: "Discount" + filter_products: "Filter Products" + delete_product_variant: "The last variant cannot be deleted!" + progress: "progress" + saving: "Saving.." + success: "success" + failure: "failure" + unsaved_changes_confirmation: "Unsaved changes will be lost. Continue anyway?" + one_product_unsaved: "Changes to one product remain unsaved." + products_unsaved: "Changes to %{n} products remain unsaved." + add_manager: "Add a manager" + is_already_manager: "is already a manager!" + no_change_to_save: " No change to save" + add_manager: "Add a manager" + users: "Users" + about: "About" + images: "Images" + contact: "Contact" + web: "Web" + primary_details: "Primary Details" + adrdress: "Address" + contact: "Contact" + social: "Social" + business_details: "Business Details" + properties: "Properties" + shipping_methods: "Shipping Methods" + payment_methods: "Payment Methods" + enterprise_fees: "Enterprise Fees" + inventory_settings: "Inventory Settings" + tag_rules: "Tag Rules" + shop_preferences: "Shop Preferences" + validation_msg_relationship_already_established: "^That relationship is already established." + validation_msg_at_least_one_hub: "^At least one hub must be selected" + validation_msg_product_category_cant_be_blank: "^Product Category cant be blank" + validation_msg_tax_category_cant_be_blank: "^Tax Category can't be blank" + validation_msg_is_associated_with_an_exising_customer: "is associated with an existing customer" + + spree: + shipment_states: + backorder: backorder + partial: partial + pending: pending + ready: ready + shipped: shipped + payment_states: + balance_due: balance due + completed: completed + checkout: checkout + credit_owed: credit owed + failed: failed + paid: paid + pending: pending + processing: processing + void: void + order_state: + address: address + adjustments: adjustments + awaiting_return: awaiting return + canceled: canceled + cart: cart + complete: complete + confirm: confirm + delivery: delivery + payment: payment + resumed: resumed + returned: returned + skrill: skrill From 9e321a63c067f41541bd7a04c83981a1209d8d41 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 2 Mar 2016 14:56:25 +1100 Subject: [PATCH 02/27] Don't put master in order cycle - we don't do that no more --- spec/features/consumer/shopping/cart_spec.rb | 2 +- spec/features/consumer/shopping/checkout_spec.rb | 3 ++- spec/features/consumer/shopping/shopping_spec.rb | 4 ++-- spec/support/request/shop_workflow.rb | 11 +++-------- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/spec/features/consumer/shopping/cart_spec.rb b/spec/features/consumer/shopping/cart_spec.rb index 58c570735d..a80fd908ff 100644 --- a/spec/features/consumer/shopping/cart_spec.rb +++ b/spec/features/consumer/shopping/cart_spec.rb @@ -11,7 +11,7 @@ feature "full-page cart", js: true do let!(:zone) { create(:zone_with_member) } let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true, charges_sales_tax: true) } let(:supplier) { create(:supplier_enterprise) } - let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) } + let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) } let(:enterprise_fee) { create(:enterprise_fee, amount: 11.00, tax_category: product.tax_category) } let(:product) { create(:taxed_product, supplier: supplier, zone: zone, price: 110.00, tax_rate_amount: 0.1) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 74145bbe1a..b1c8856f8d 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -11,9 +11,10 @@ feature "As a consumer I want to check out my cart", js: true do let!(:zone) { create(:zone_with_member) } let(:distributor) { create(:distributor_enterprise, charges_sales_tax: true) } let(:supplier) { create(:supplier_enterprise) } - let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) } + let!(:order_cycle) { create(:simple_order_cycle, suppliers: [supplier], distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [variant]) } let(:enterprise_fee) { create(:enterprise_fee, amount: 1.23, tax_category: product.tax_category) } let(:product) { create(:taxed_product, supplier: supplier, price: 10, zone: zone, tax_rate_amount: 0.1) } + let(:variant) { product.variants.first } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } before do diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index a8545af3db..8fa78f6006 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -182,7 +182,7 @@ feature "As a consumer I want to shop with a distributor", js: true do describe "with variants on the product" do let(:variant) { create(:variant, product: product, on_hand: 10 ) } before do - add_product_and_variant_to_order_cycle(exchange, product, variant) + add_variant_to_order_cycle(exchange, variant) set_order_cycle(order, oc1) visit shop_path end @@ -217,7 +217,7 @@ feature "As a consumer I want to shop with a distributor", js: true do let(:variant) { create(:variant, product: product) } before do - add_product_and_variant_to_order_cycle(exchange, product, variant) + add_variant_to_order_cycle(exchange, variant) set_order_cycle(order, oc1) visit shop_path end diff --git a/spec/support/request/shop_workflow.rb b/spec/support/request/shop_workflow.rb index a0e6ab846c..279ead2630 100644 --- a/spec/support/request/shop_workflow.rb +++ b/spec/support/request/shop_workflow.rb @@ -18,7 +18,7 @@ module ShopWorkflow def add_product_to_cart populator = Spree::OrderPopulator.new(order, order.currency) - populator.populate(variants: {product.master.id => 1}) + populator.populate(variants: {product.variants.first.id => 1}) # Recalculate fee totals order.update_distribution_charge! @@ -28,15 +28,10 @@ module ShopWorkflow find("dd a", text: name).trigger "click" end - def add_product_to_order_cycle(exchange, product) - exchange.variants << product.master + def add_variant_to_order_cycle(exchange, variant) + exchange.variants << variant end - def add_product_and_variant_to_order_cycle(exchange, product, variant) - exchange.variants << product.master - exchange.variants << variant - end - def set_order_cycle(order, order_cycle) order.update_attribute(:order_cycle, order_cycle) end From 243f59c87d9a645ef58e7e1b59f247663b96dffd Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 2 Mar 2016 15:23:38 +1100 Subject: [PATCH 03/27] When there's an out of stock product in the cart, checkout returns user to cart --- app/controllers/checkout_controller.rb | 2 +- spec/features/consumer/shopping/checkout_spec.rb | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 1cffc05734..3ac826df52 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -152,7 +152,7 @@ class CheckoutController < Spree::CheckoutController # Overriding Spree's methods def raise_insufficient_quantity flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) - redirect_to main_app.shop_path + redirect_to cart_path end def redirect_to_paypal_express_form_if_needed diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index b1c8856f8d..2cd959a1b4 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -46,6 +46,22 @@ feature "As a consumer I want to check out my cart", js: true do distributor.shipping_methods << sm3 end + describe "when I have an out of stock product in my cart" do + before do + Spree::Config.set allow_backorders: false + variant.on_hand = 0 + variant.save! + end + + it "returns me to the cart with an error message" do + visit checkout_path + + page.should_not have_selector 'closing', text: "Checkout now" + page.should have_selector 'closing', text: "Your shopping cart" + page.should have_content "An item in your cart has become unavailable" + end + end + context "on the checkout page" do before do visit checkout_path From d45b5254971050093cd20348da195834a0c65661 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Wed, 9 Mar 2016 15:33:37 +1100 Subject: [PATCH 04/27] When there's an out of stock product in the cart, placing order returns user to the cart --- .../darkswarm/services/checkout.js.coffee | 9 ++-- app/controllers/checkout_controller.rb | 13 +++++- .../consumer/shopping/checkout_spec.rb | 12 ++++++ .../services/checkout_spec.js.coffee | 41 +++++++++++-------- 4 files changed, 53 insertions(+), 22 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/checkout.js.coffee b/app/assets/javascripts/darkswarm/services/checkout.js.coffee index 652ecd02f9..34cae22fcc 100644 --- a/app/assets/javascripts/darkswarm/services/checkout.js.coffee +++ b/app/assets/javascripts/darkswarm/services/checkout.js.coffee @@ -10,9 +10,12 @@ Darkswarm.factory 'Checkout', (CurrentOrder, ShippingMethods, PaymentMethods, $h $http.put('/checkout', {order: @preprocess()}).success (data, status)=> Navigation.go data.path .error (response, status)=> - Loading.clear() - @errors = response.errors - RailsFlashLoader.loadFlash(response.flash) + if response.path + Navigation.go response.path + else + Loading.clear() + @errors = response.errors + RailsFlashLoader.loadFlash(response.flash) # Rails wants our Spree::Address data to be provided with _attributes preprocess: -> diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 3ac826df52..7b34bbcff5 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -151,8 +151,17 @@ class CheckoutController < Spree::CheckoutController # Overriding Spree's methods def raise_insufficient_quantity - flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) - redirect_to cart_path + respond_to do |format| + format.html do + flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) + redirect_to cart_path + end + + format.json do + flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) + render json: {path: cart_path}, status: 400 + end + end end def redirect_to_paypal_express_form_if_needed diff --git a/spec/features/consumer/shopping/checkout_spec.rb b/spec/features/consumer/shopping/checkout_spec.rb index 2cd959a1b4..9e9d830a50 100644 --- a/spec/features/consumer/shopping/checkout_spec.rb +++ b/spec/features/consumer/shopping/checkout_spec.rb @@ -230,6 +230,18 @@ feature "As a consumer I want to check out my cart", js: true do page.should have_content "Your order has been processed successfully" end + it "takes us to the cart page with an error when a product becomes out of stock just before we purchase", js: true do + Spree::Config.set allow_backorders: false + variant.on_hand = 0 + variant.save! + + place_order + + page.should_not have_content "Your order has been processed successfully" + page.should have_selector 'closing', text: "Your shopping cart" + page.should have_content "Out of Stock" + end + context "when we are charged a shipping fee" do before { choose sm2.name } diff --git a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee index e3ba3f51de..718504a377 100644 --- a/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/checkout_spec.js.coffee @@ -5,7 +5,7 @@ describe 'Checkout service', -> Navigation = null flash = null scope = null - FlashLoaderMock = + FlashLoaderMock = loadFlash: (arg)-> paymentMethods = [{ id: 99 @@ -41,10 +41,10 @@ describe 'Checkout service', -> module 'Darkswarm' module ($provide)-> - $provide.value "RailsFlashLoader", FlashLoaderMock - $provide.value "currentOrder", orderData - $provide.value "shippingMethods", shippingMethods - $provide.value "paymentMethods", paymentMethods + $provide.value "RailsFlashLoader", FlashLoaderMock + $provide.value "currentOrder", orderData + $provide.value "shippingMethods", shippingMethods + $provide.value "paymentMethods", paymentMethods null inject ($injector, _$httpBackend_, $rootScope)-> @@ -80,26 +80,33 @@ describe 'Checkout service', -> it 'Gets the current payment method', -> expect(Checkout.paymentMethod()).toEqual null Checkout.order.payment_method_id = 99 - expect(Checkout.paymentMethod()).toEqual paymentMethods[0] + expect(Checkout.paymentMethod()).toEqual paymentMethods[0] it "Posts the Checkout to the server", -> $httpBackend.expectPUT("/checkout", {order: Checkout.preprocess()}).respond 200, {path: "test"} Checkout.submit() $httpBackend.flush() - it "sends flash messages to the flash service", -> - spyOn(FlashLoaderMock, "loadFlash") # Stubbing out writes to window.location - $httpBackend.expectPUT("/checkout").respond 400, {flash: {error: "frogs"}} - Checkout.submit() + describe "when there is an error", -> + it "redirects when a redirect is given", -> + $httpBackend.expectPUT("/checkout").respond 400, {path: 'path'} + Checkout.submit() + $httpBackend.flush() + expect(Navigation.go).toHaveBeenCalledWith 'path' - $httpBackend.flush() - expect(FlashLoaderMock.loadFlash).toHaveBeenCalledWith {error: "frogs"} + it "sends flash messages to the flash service", -> + spyOn(FlashLoaderMock, "loadFlash") # Stubbing out writes to window.location + $httpBackend.expectPUT("/checkout").respond 400, {flash: {error: "frogs"}} + Checkout.submit() - it "puts errors into the scope", -> - $httpBackend.expectPUT("/checkout").respond 400, {errors: {error: "frogs"}} - Checkout.submit() - $httpBackend.flush() - expect(Checkout.errors).toEqual {error: "frogs"} + $httpBackend.flush() + expect(FlashLoaderMock.loadFlash).toHaveBeenCalledWith {error: "frogs"} + + it "puts errors into the scope", -> + $httpBackend.expectPUT("/checkout").respond 400, {errors: {error: "frogs"}} + Checkout.submit() + $httpBackend.flush() + expect(Checkout.errors).toEqual {error: "frogs"} describe "data preprocessing", -> beforeEach -> From 17f69bd182e61590fbef23871c1e0e18141af13a Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 24 Mar 2016 11:56:26 +1100 Subject: [PATCH 05/27] Remove trailing whitespace --- .../templates/shop_variant.html.haml | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/assets/javascripts/templates/shop_variant.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml index 00e4f70d7c..83463da328 100644 --- a/app/assets/javascripts/templates/shop_variant.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -1,6 +1,6 @@ .variants.row .small-12.medium-4.large-4.columns.variant-name - .table-cell + .table-cell .inline {{ variant.name_to_display }} .bulk-buy.inline{"bo-if" => "variant.product.group_buy"} %i.ofn-i_056-bulk>< @@ -10,25 +10,25 @@ -# WITHOUT GROUP BUY .small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"} - %input{type: :number, + %input{type: :number, integer: true, - value: nil, - min: 0, + value: nil, + min: 0, placeholder: "0", "ofn-disable-scroll" => true, "ng-model" => "variant.line_item.quantity", max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} - + -# WITH GROUP BUY .small-5.medium-3.large-3.columns.text-right{"bo-if" => "variant.product.group_buy"} %span.bulk-input-container %span.bulk-input - %input.bulk.first{type: :number, - value: nil, + %input.bulk.first{type: :number, + value: nil, integer: true, - min: 0, + min: 0, "ng-model" => "variant.line_item.quantity", placeholder: "{{'shop_variant_quantity_min' | t}}", "ofn-disable-scroll" => true, @@ -46,16 +46,16 @@ name: "variant_attributes[{{variant.id}}][max_quantity]"} .small-3.medium-1.large-1.columns.variant-unit - .table-cell + .table-cell %em {{ variant.unit_to_display }} .small-4.medium-2.large-2.columns.variant-price .table-cell.price - %i.ofn-i_009-close + %i.ofn-i_009-close {{ variant.price_with_fees | localizeCurrency }} -# Now in a template in app/assets/javascripts/templates ! - %price-breakdown{"price-breakdown" => "_", variant: "variant", + %price-breakdown{"price-breakdown" => "_", variant: "variant", "price-breakdown-append-to-body" => "true", "price-breakdown-placement" => "left", "price-breakdown-animation" => true} @@ -63,4 +63,4 @@ .small-12.medium-2.large-2.columns.total-price.text-right .table-cell %strong{"ng-class" => "{filled: variant.totalPrice()}"} - {{ variant.totalPrice() | localizeCurrency }} + {{ variant.totalPrice() | localizeCurrency }} From a1bcdc616f2d857720a25721d508afe38413ce06 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 24 Mar 2016 12:05:16 +1100 Subject: [PATCH 06/27] Extract add-to-cart inputs into partials --- .../shop_variant_no_group_buy.html.haml | 11 ++++++ .../shop_variant_with_group_buy.html.haml | 22 +++++++++++ .../templates/shop_variant.html.haml | 37 +------------------ 3 files changed, 35 insertions(+), 35 deletions(-) create mode 100644 app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml create mode 100644 app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml diff --git a/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml b/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml new file mode 100644 index 0000000000..4ce584aa42 --- /dev/null +++ b/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml @@ -0,0 +1,11 @@ +.small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"} + + %input{type: :number, + integer: true, + value: nil, + min: 0, + placeholder: "0", + "ofn-disable-scroll" => true, + "ng-model" => "variant.line_item.quantity", + max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", + name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} diff --git a/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml b/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml new file mode 100644 index 0000000000..51f7599af7 --- /dev/null +++ b/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml @@ -0,0 +1,22 @@ +.small-5.medium-3.large-3.columns.text-right{"bo-if" => "variant.product.group_buy"} + %span.bulk-input-container + %span.bulk-input + %input.bulk.first{type: :number, + value: nil, + integer: true, + min: 0, + "ng-model" => "variant.line_item.quantity", + placeholder: "{{'shop_variant_quantity_min' | t}}", + "ofn-disable-scroll" => true, + max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", + name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} + %span.bulk-input + %input.bulk.second{type: :number, + "ng-disabled" => "!variant.line_item.quantity", + integer: true, + min: 0, + "ng-model" => "variant.line_item.max_quantity", + placeholder: "{{'shop_variant_quantity_max' | t}}", + "ofn-disable-scroll" => true, + max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", + name: "variant_attributes[{{variant.id}}][max_quantity]"} diff --git a/app/assets/javascripts/templates/shop_variant.html.haml b/app/assets/javascripts/templates/shop_variant.html.haml index 83463da328..2c9eb932d5 100644 --- a/app/assets/javascripts/templates/shop_variant.html.haml +++ b/app/assets/javascripts/templates/shop_variant.html.haml @@ -7,44 +7,11 @@ %em>< \ {{'bulk' | t}} - -# WITHOUT GROUP BUY - .small-5.medium-3.large-3.columns.text-right{"bo-if" => "!variant.product.group_buy"} - %input{type: :number, - integer: true, - value: nil, - min: 0, - placeholder: "0", - "ofn-disable-scroll" => true, - "ng-model" => "variant.line_item.quantity", - max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", - name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} + %ng-include{src: "'partials/shop_variant_no_group_buy.html'"} + %ng-include{src: "'partials/shop_variant_with_group_buy.html'"} - -# WITH GROUP BUY - .small-5.medium-3.large-3.columns.text-right{"bo-if" => "variant.product.group_buy"} - %span.bulk-input-container - %span.bulk-input - %input.bulk.first{type: :number, - value: nil, - integer: true, - min: 0, - "ng-model" => "variant.line_item.quantity", - placeholder: "{{'shop_variant_quantity_min' | t}}", - "ofn-disable-scroll" => true, - max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", - name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} - %span.bulk-input - %input.bulk.second{type: :number, - "ng-disabled" => "!variant.line_item.quantity", - integer: true, - min: 0, - "ng-model" => "variant.line_item.max_quantity", - placeholder: "{{'shop_variant_quantity_max' | t}}", - "ofn-disable-scroll" => true, - max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", - name: "variant_attributes[{{variant.id}}][max_quantity]"} - .small-3.medium-1.large-1.columns.variant-unit .table-cell %em {{ variant.unit_to_display }} From 292d02749800620ce5ac71f4f7b24e8be1a08d93 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 24 Mar 2016 14:48:13 +1100 Subject: [PATCH 07/27] When adding an item to cart with not enough stock, add as much as we can without erroring --- app/models/spree/order_populator_decorator.rb | 25 ++++++-- spec/models/spree/order_populator_spec.rb | 60 +++++++++++++++++-- 2 files changed, 76 insertions(+), 9 deletions(-) diff --git a/app/models/spree/order_populator_decorator.rb b/app/models/spree/order_populator_decorator.rb index 1ca0f9efc2..7ff64cdf71 100644 --- a/app/models/spree/order_populator_decorator.rb +++ b/app/models/spree/order_populator_decorator.rb @@ -49,17 +49,32 @@ Spree::OrderPopulator.class_eval do def attempt_cart_add(variant_id, quantity, max_quantity = nil) quantity = quantity.to_i + max_quantity = max_quantity.to_i if max_quantity variant = Spree::Variant.find(variant_id) OpenFoodNetwork::ScopeVariantToHub.new(@distributor).scope(variant) - if quantity > 0 - if check_stock_levels(variant, quantity) && - check_order_cycle_provided_for(variant) && - check_variant_available_under_distribution(variant) - @order.add_variant(variant, quantity, max_quantity, currency) + if quantity > 0 && + check_order_cycle_provided_for(variant) && + check_variant_available_under_distribution(variant) + + quantity_to_add, max_quantity_to_add = quantities_to_add(variant, quantity, max_quantity) + + if quantity_to_add > 0 + @order.add_variant(variant, quantity_to_add, max_quantity_to_add, currency) end end end + def quantities_to_add(variant, quantity, max_quantity) + # If not enough stock is available, add as much as we can to the cart + on_hand = variant.on_hand + on_hand = [quantity, max_quantity].compact.max if Spree::Config.allow_backorders + quantity_to_add = [quantity, on_hand].min + max_quantity_to_add = [max_quantity, on_hand].min if max_quantity + + [quantity_to_add, max_quantity_to_add] + end + + def cart_remove(variant_id) variant = Spree::Variant.find(variant_id) @order.remove_variant(variant) diff --git a/spec/models/spree/order_populator_spec.rb b/spec/models/spree/order_populator_spec.rb index 7ba59fb47a..389c037b2e 100644 --- a/spec/models/spree/order_populator_spec.rb +++ b/spec/models/spree/order_populator_spec.rb @@ -149,13 +149,15 @@ module Spree end describe "attempt_cart_add" do - it "performs additional validations" do - variant = double(:variant) - quantity = 123 + let(:variant) { double(:variant, on_hand: 250) } + let(:quantity) { 123 } + + before do Spree::Variant.stub(:find).and_return(variant) VariantOverride.stub(:for).and_return(nil) + end - op.should_receive(:check_stock_levels).with(variant, quantity).and_return(true) + it "performs additional validations" do op.should_receive(:check_order_cycle_provided_for).with(variant).and_return(true) op.should_receive(:check_variant_available_under_distribution).with(variant). and_return(true) @@ -163,8 +165,58 @@ module Spree op.attempt_cart_add(333, quantity.to_s) end + + it "filters quantities through #quantities_to_add" do + op.should_receive(:quantities_to_add).with(variant, 123, 123). + and_return([5, 5]) + + op.stub(:check_order_cycle_provided_for) { true } + op.stub(:check_variant_available_under_distribution) { true } + + order.should_receive(:add_variant).with(variant, 5, 5, currency) + + op.attempt_cart_add(333, quantity.to_s, quantity.to_s) + end end + describe "quantities_to_add" do + let(:v) { double(:variant, on_hand: 10) } + context "when max_quantity is not provided" do + it "returns full amount when available" do + op.quantities_to_add(v, 5, nil).should == [5, nil] + end + + it "returns a limited amount when not entirely available" do + op.quantities_to_add(v, 15, nil).should == [10, nil] + end + end + + context "when max_quantity is provided" do + it "returns full amount when available" do + op.quantities_to_add(v, 5, 6).should == [5, 6] + end + + it "returns a limited amount when not entirely available" do + op.quantities_to_add(v, 15, 16).should == [10, 10] + end + end + + context "when backorders are allowed" do + around do |example| + Spree::Config.allow_backorders = true + example.run + Spree::Config.allow_backorders = false + end + + it "does not limit quantity" do + op.quantities_to_add(v, 15, nil).should == [15, nil] + end + + it "does not limit max_quantity" do + op.quantities_to_add(v, 15, 16).should == [15, 16] + end + end + end describe "validations" do describe "determining if distributor can supply products in cart" do From fee0f90a1b90251c7a8b6f969c35ed2eef3bb66f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 24 Mar 2016 15:48:35 +1100 Subject: [PATCH 08/27] After adding products to cart, return status of cart and available stock levels --- .../spree/orders_controller_decorator.rb | 18 ++++++++-- .../spree/orders_controller_spec.rb | 34 +++++++++++++++++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index c6d325d0f8..4c2ab8cb63 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -30,19 +30,33 @@ Spree::OrdersController.class_eval do Spree::Adjustment.without_callbacks do populator = Spree::OrderPopulator.new(current_order(true), current_currency) + if populator.populate(params.slice(:products, :variants, :quantity), true) fire_event('spree.cart.add') fire_event('spree.order.contents_changed') current_order.update! - render json: true, status: 200 + render json: {error: false, stock_levels: stock_levels}, status: 200 + else - render json: false, status: 402 + render json: {error: true}, status: 412 end end end + def stock_levels + Hash[ + current_order.line_items.map do |li| + [li.variant.id, + {quantity: li.quantity, + max_quantity: li.max_quantity, + on_hand: li.variant.on_hand}] + end + ] + end + + def update_distribution @order = current_order(true) diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index 73c72f3386..756be1d3ec 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -42,6 +42,36 @@ describe Spree::OrdersController do flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later." end + describe "returning stock levels in JSON on success" do + let(:product) { create(:simple_product) } + + it "returns stock levels as JSON" do + controller.stub(:stock_levels) { 'my_stock_levels' } + Spree::OrderPopulator.stub(:new).and_return(populator = double()) + populator.stub(:populate) { true } + + xhr :post, :populate, use_route: :spree, format: :json + + data = JSON.parse(response.body) + data['stock_levels'].should == 'my_stock_levels' + end + + describe "generating stock levels" do + let!(:order) { create(:order) } + let!(:li) { create(:line_item, order: order, variant: v, quantity: 2, max_quantity: 3) } + let!(:v) { create(:variant, count_on_hand: 4) } + + before do + order.reload + controller.stub(:current_order) { order } + end + + it "returns a hash with variant id, quantity, max_quantity and stock on hand" do + controller.stock_levels.should == {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} + end + end + end + context "adding a group buy product to the cart" do it "sets a variant attribute for the max quantity" do distributor_product = create(:distributor_enterprise) @@ -68,7 +98,7 @@ describe Spree::OrdersController do Spree::OrderPopulator.stub(:new).and_return(populator = double()) populator.stub(:populate).and_return false xhr :post, :populate, use_route: :spree, format: :json - response.status.should == 402 + response.status.should == 412 end it "tells populator to overwrite" do @@ -78,7 +108,7 @@ describe Spree::OrdersController do end end - context "removing line items from cart" do + describe "removing line items from cart" do describe "when I pass params that includes a line item no longer in our cart" do it "should silently ignore the missing line item" do order = subject.current_order(true) From 8a62d26af4728bd709d174a443be143170321b8d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 24 Mar 2016 16:24:36 +1100 Subject: [PATCH 09/27] After adding an item to the cart, when out of stock, remove from cart and reset client-side stock level --- .../darkswarm/services/cart.js.coffee | 11 ++++++++++ .../darkswarm/services/cart_spec.js.coffee | 21 +++++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index f1c9421a15..c9fdd1471b 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -31,12 +31,23 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)-> $http.post('/orders/populate', @data()).success (data, status)=> @saved() @update_running = false + + @compareAndNotifyStockLevels data.stock_levels + @popQueue() if @update_enqueued .error (response, status)=> @scheduleRetry(status) @update_running = false + compareAndNotifyStockLevels: (stockLevels) => + for li in @line_items_present() + if !stockLevels[li.variant.id]? + alert "Variant out of stock: #{li.variant.id}" + li.quantity = 0 + li.max_quantity = 0 if li.max_quantity? + li.variant.count_on_hand = 0 + popQueue: => @update_enqueued = false @scheduleUpdate() diff --git a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee index 518a524010..41cee8de4b 100644 --- a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee @@ -126,6 +126,27 @@ describe 'Cart service', -> $httpBackend.flush() expect(Cart.scheduleRetry).toHaveBeenCalled() + describe "verifying stock levels after update", -> + describe "when an item is out of stock", -> + it "reduces the quantity in the cart", -> + li = {variant: {id: 1}, quantity: 5} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels({}) + expect(li.quantity).toEqual 0 + expect(li.max_quantity).toBeUndefined() + + it "reduces the max_quantity in the cart", -> + li = {variant: {id: 1}, quantity: 5, max_quantity: 6} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels({}) + expect(li.max_quantity).toEqual 0 + + it "resets the count on hand available", -> + li = {variant: {id: 1, count_on_hand: 10}, quantity: 5} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels({}) + expect(li.variant.count_on_hand).toEqual 0 + it "pops the queue", -> Cart.update_enqueued = true spyOn(Cart, 'scheduleUpdate') From 6fbbe580c5bfc35c2455bb8ca19c834628d8efaf Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 24 Mar 2016 16:41:54 +1100 Subject: [PATCH 10/27] After adding an item to the cart, when less quantity available, reduce quantity and reset client-side stock level --- .../darkswarm/services/cart.js.coffee | 8 +++++ .../darkswarm/services/cart_spec.js.coffee | 29 +++++++++++++++++-- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index c9fdd1471b..973170e414 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -47,6 +47,14 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)-> li.quantity = 0 li.max_quantity = 0 if li.max_quantity? li.variant.count_on_hand = 0 + else + if stockLevels[li.variant.id].quantity < li.quantity + alert "Variant quantity reduced: #{li.variant.id}" + li.quantity = stockLevels[li.variant.id].quantity + if stockLevels[li.variant.id].max_quantity < li.max_quantity + alert "Variant max_quantity reduced: #{li.variant.id}" + li.max_quantity = stockLevels[li.variant.id].max_quantity + li.variant.count_on_hand = stockLevels[li.variant.id].on_hand popQueue: => @update_enqueued = false diff --git a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee index 41cee8de4b..58b7795124 100644 --- a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee @@ -131,22 +131,45 @@ describe 'Cart service', -> it "reduces the quantity in the cart", -> li = {variant: {id: 1}, quantity: 5} spyOn(Cart, 'line_items_present').andReturn [li] - Cart.compareAndNotifyStockLevels({}) + Cart.compareAndNotifyStockLevels {} expect(li.quantity).toEqual 0 expect(li.max_quantity).toBeUndefined() it "reduces the max_quantity in the cart", -> li = {variant: {id: 1}, quantity: 5, max_quantity: 6} spyOn(Cart, 'line_items_present').andReturn [li] - Cart.compareAndNotifyStockLevels({}) + Cart.compareAndNotifyStockLevels {} expect(li.max_quantity).toEqual 0 it "resets the count on hand available", -> li = {variant: {id: 1, count_on_hand: 10}, quantity: 5} spyOn(Cart, 'line_items_present').andReturn [li] - Cart.compareAndNotifyStockLevels({}) + Cart.compareAndNotifyStockLevels {} expect(li.variant.count_on_hand).toEqual 0 + describe "when the quantity available is less than that requested", -> + it "reduces the quantity in the cart", -> + li = {variant: {id: 1}, quantity: 6} + stockLevels = {1: {quantity: 5, on_hand: 5}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.quantity).toEqual 5 + expect(li.max_quantity).toBeUndefined() + + it "reduces the max_quantity in the cart", -> + li = {variant: {id: 1}, quantity: 6, max_quantity: 7} + stockLevels = {1: {quantity: 5, max_quantity: 5, on_hand: 5}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.max_quantity).toEqual 5 + + it "resets the count on hand available", -> + li = {variant: {id: 1}, quantity: 6} + stockLevels = {1: {quantity: 5, on_hand: 6}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.variant.count_on_hand).toEqual 6 + it "pops the queue", -> Cart.update_enqueued = true spyOn(Cart, 'scheduleUpdate') From 792e17c385cd3cc3a3d6d3d2c0fb22983df1ea7d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 8 Apr 2016 11:17:54 +1000 Subject: [PATCH 11/27] When removing variant from order, if not found then do nothing --- app/models/spree/order_decorator.rb | 2 +- spec/models/spree/order_spec.rb | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index f0e9324185..53a1b81873 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -99,7 +99,7 @@ Spree::Order.class_eval do def remove_variant(variant) line_items(:reload) current_item = find_line_item_by_variant(variant) - current_item.destroy + current_item.andand.destroy end diff --git a/spec/models/spree/order_spec.rb b/spec/models/spree/order_spec.rb index ab86be1580..4db0541d6c 100644 --- a/spec/models/spree/order_spec.rb +++ b/spec/models/spree/order_spec.rb @@ -366,6 +366,7 @@ describe Spree::Order do let(:order) { create(:order) } let(:v1) { create(:variant) } let(:v2) { create(:variant) } + let(:v3) { create(:variant) } before do order.add_variant v1 @@ -376,6 +377,12 @@ describe Spree::Order do order.remove_variant v1 order.line_items(:reload).map(&:variant).should == [v2] end + + it "does nothing when there is no matching line item" do + expect do + order.remove_variant v3 + end.to change(order.line_items(:reload), :count).by(0) + end end describe "emptying the order" do From 8695dea0a5c36fd4c6753e89d0c93b5a652dacba Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 8 Apr 2016 11:19:34 +1000 Subject: [PATCH 12/27] Remove variant from cart when it becomes out of stock --- app/models/spree/order_populator_decorator.rb | 2 ++ spec/models/spree/order_populator_spec.rb | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/app/models/spree/order_populator_decorator.rb b/app/models/spree/order_populator_decorator.rb index 7ff64cdf71..82d9aa85e7 100644 --- a/app/models/spree/order_populator_decorator.rb +++ b/app/models/spree/order_populator_decorator.rb @@ -60,6 +60,8 @@ Spree::OrderPopulator.class_eval do if quantity_to_add > 0 @order.add_variant(variant, quantity_to_add, max_quantity_to_add, currency) + else + @order.remove_variant variant end end end diff --git a/spec/models/spree/order_populator_spec.rb b/spec/models/spree/order_populator_spec.rb index 389c037b2e..9f7cc47c51 100644 --- a/spec/models/spree/order_populator_spec.rb +++ b/spec/models/spree/order_populator_spec.rb @@ -177,6 +177,19 @@ module Spree op.attempt_cart_add(333, quantity.to_s, quantity.to_s) end + + it "removes variants which have become out of stock" do + op.should_receive(:quantities_to_add).with(variant, 123, 123). + and_return([0, 0]) + + op.stub(:check_order_cycle_provided_for) { true } + op.stub(:check_variant_available_under_distribution) { true } + + order.should_receive(:remove_variant).with(variant) + order.should_receive(:add_variant).never + + op.attempt_cart_add(333, quantity.to_s, quantity.to_s) + end end describe "quantities_to_add" do From cfe062918b3580c20a65a13b3c7c7c2d50a726f6 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 8 Apr 2016 11:22:06 +1000 Subject: [PATCH 13/27] When a variant goes out of stock, disable the input and grey out the row --- .../shop_variant_no_group_buy.html.haml | 1 + .../darkswarm/_shop-product-rows.css.sass | 28 ++++++++-------- app/views/shop/products/_form.html.haml | 4 +-- .../consumer/shopping/shopping_spec.rb | 33 +++++++++++++++++++ 4 files changed, 51 insertions(+), 15 deletions(-) diff --git a/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml b/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml index 4ce584aa42..36a4e44ade 100644 --- a/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml +++ b/app/assets/javascripts/templates/partials/shop_variant_no_group_buy.html.haml @@ -8,4 +8,5 @@ "ofn-disable-scroll" => true, "ng-model" => "variant.line_item.quantity", max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", + "ng-disabled" => "!variant.on_demand && variant.count_on_hand == 0", name: "variants[{{variant.id}}]", id: "variants_{{variant.id}}"} diff --git a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass index c67b030292..c0e4fb27aa 100644 --- a/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass +++ b/app/assets/stylesheets/darkswarm/_shop-product-rows.css.sass @@ -11,20 +11,20 @@ padding-bottom: 0em display: table line-height: 1.1 - // outline: 1px solid red + // outline: 1px solid red - @media all and (max-width: 768px) - font-size: 0.875rem + @media all and (max-width: 768px) + font-size: 0.875rem + + @media all and (max-width: 640px) + font-size: 0.75rem - @media all and (max-width: 640px) - font-size: 0.75rem - .table-cell display: table-cell vertical-align: middle height: 37px - // ROW VARIANTS + // ROW VARIANTS .row.variants margin-left: 0 margin-right: 0 @@ -35,7 +35,10 @@ background-color: #f9f9f9 &:hover, &:focus, &:active background-color: $clr-brick-ultra-light - + + &.out-of-stock + opacity: 0.2 + // Variant name .variant-name padding-left: 7.9375rem @@ -52,7 +55,7 @@ height: 27px // Variant unit - .variant-unit + .variant-unit padding-left: 0rem padding-right: 0rem color: #888 @@ -88,18 +91,18 @@ margin-left: 0 margin-right: 0 background: #fff - + .columns padding-top: 1em padding-bottom: 1em line-height: 1 - + @media all and (max-width: 768px) padding-top: 0.65rem padding-bottom: 0.65rem .summary-header - padding-left: 7.9375rem + padding-left: 7.9375rem @media all and (max-width: 768px) padding-left: 4.9375rem @media all and (max-width: 640px) @@ -118,4 +121,3 @@ color: $clr-brick i font-size: 0.8em - diff --git a/app/views/shop/products/_form.html.haml b/app/views/shop/products/_form.html.haml index 18868e5c77..f2be498451 100644 --- a/app/views/shop/products/_form.html.haml +++ b/app/views/shop/products/_form.html.haml @@ -32,9 +32,9 @@ %div.pad-top{bindonce: true} %product.animate-repeat{"ng-controller" => "ProductNodeCtrl", "ng-repeat" => "product in filteredProducts = (Products.products | products:query | taxons:activeTaxons | properties: activeProperties) track by product.id ", "id" => "product-{{ product.id }}"} - = render partial: "shop/products/summary" + = render "shop/products/summary" %shop-variant{variant: 'product.master', "bo-if" => "!product.hasVariants", "id" => "variant-{{ product.master.id }}"} - %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}"} + %shop-variant{variant: 'variant', "ng-repeat" => "variant in product.variants track by variant.id", "id" => "variant-{{ variant.id }}", "ng-class" => "{'out-of-stock': !variant.on_demand && variant.count_on_hand == 0}"} %product{"ng-show" => "Products.loading"} .row.summary diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 8fa78f6006..ab94c678d7 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -215,9 +215,11 @@ feature "As a consumer I want to shop with a distributor", js: true do let(:exchange) { Exchange.find(oc1.exchanges.to_enterprises(distributor).outgoing.first.id) } let(:product) { create(:simple_product) } let(:variant) { create(:variant, product: product) } + let(:variant2) { create(:variant, product: product) } before do add_variant_to_order_cycle(exchange, variant) + add_variant_to_order_cycle(exchange, variant2) set_order_cycle(order, oc1) visit shop_path end @@ -235,6 +237,37 @@ feature "As a consumer I want to shop with a distributor", js: true do Spree::LineItem.where(id: li).should be_empty end + + describe "when a product goes out of stock just before it's added to the cart" do + before do + variant.update_attributes! on_hand: 0 + end + + it "stops the attempt, shows an error message and refreshes the products asynchronously" do + # -- Messaging + alert_message = accept_alert do + fill_in "variants[#{variant.id}]", with: '1' + wait_until { !cart_dirty } + end + + # TODO: This will be a modal + expect(alert_message).to be + puts alert_message + + # -- Page updates + # Update amount in cart + page.should have_field "variants[#{variant.id}]", with: '0', disabled: true + page.should have_field "variants[#{variant2.id}]", with: '' + + # Update amount available in product list + # If amount falls to zero, variant should be greyed out and input disabled + page.should have_selector "#variant-#{variant.id}.out-of-stock" + page.should have_selector "#variants_#{variant.id}[max='0']" + page.should have_selector "#variants_#{variant.id}[disabled='disabled']" + end + + it "does the same for group buy products" + end end context "when no order cycles are available" do From 5e39b11c2f2d81274f3f6fff956d3e84dfae0fc3 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 8 Apr 2016 11:44:28 +1000 Subject: [PATCH 14/27] Spec out of stock handling for group buy --- .../shop_variant_with_group_buy.html.haml | 3 +- .../consumer/shopping/shopping_spec.rb | 37 ++++++++++++++++--- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml b/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml index 51f7599af7..c44a71948b 100644 --- a/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml +++ b/app/assets/javascripts/templates/partials/shop_variant_with_group_buy.html.haml @@ -19,4 +19,5 @@ placeholder: "{{'shop_variant_quantity_max' | t}}", "ofn-disable-scroll" => true, max: "{{variant.on_demand && 9999 || variant.count_on_hand }}", - name: "variant_attributes[{{variant.id}}][max_quantity]"} + name: "variant_attributes[{{variant.id}}][max_quantity]", + id: "variants_{{variant.id}}_max"} diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index ab94c678d7..c252a3f041 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -239,11 +239,9 @@ feature "As a consumer I want to shop with a distributor", js: true do end describe "when a product goes out of stock just before it's added to the cart" do - before do - variant.update_attributes! on_hand: 0 - end - it "stops the attempt, shows an error message and refreshes the products asynchronously" do + variant.update_attributes! on_hand: 0 + # -- Messaging alert_message = accept_alert do fill_in "variants[#{variant.id}]", with: '1' @@ -266,7 +264,36 @@ feature "As a consumer I want to shop with a distributor", js: true do page.should have_selector "#variants_#{variant.id}[disabled='disabled']" end - it "does the same for group buy products" + context "group buy products" do + let(:product) { create(:simple_product, group_buy: true) } + + it "does the same" do + # -- Place in cart so we can set max_quantity, then make out of stock + fill_in "variants[#{variant.id}]", with: '1' + wait_until { !cart_dirty } + variant.update_attributes! on_hand: 0 + + # -- Messaging + alert_message = accept_alert do + fill_in "variant_attributes[#{variant.id}][max_quantity]", with: '1' + wait_until { !cart_dirty } + end + + # TODO: This will be a modal + expect(alert_message).to be + puts alert_message + + # -- Page updates + # Update amount in cart + page.should have_field "variant_attributes[#{variant.id}][max_quantity]", with: '0', disabled: true + + # Update amount available in product list + # If amount falls to zero, variant should be greyed out and input disabled + page.should have_selector "#variant-#{variant.id}.out-of-stock" + page.should have_selector "#variants_#{variant.id}_max[max='0']" + page.should have_selector "#variants_#{variant.id}_max[disabled='disabled']" + end + end end end From 35117f7af433bc31f1a9be9cacf2f966b2d3b28b Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 8 Apr 2016 15:39:25 +1000 Subject: [PATCH 15/27] Show a modal when available stock levels have reduced --- .../darkswarm/services/cart.js.coffee | 17 +++++++++--- .../templates/out_of_stock.html.haml | 10 +++++++ .../spree/orders_controller_decorator.rb | 8 ++++-- .../spree/orders_controller_spec.rb | 8 ++++++ .../consumer/shopping/shopping_spec.rb | 26 +++++++++---------- 5 files changed, 49 insertions(+), 20 deletions(-) create mode 100644 app/assets/javascripts/templates/out_of_stock.html.haml diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index 973170e414..4f808e0710 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -1,4 +1,4 @@ -Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)-> +Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $rootScope, storage)-> # Handles syncing of current cart/order state to server new class Cart dirty: false @@ -41,21 +41,30 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, storage)-> @update_running = false compareAndNotifyStockLevels: (stockLevels) => + scope = $rootScope.$new(true) + scope.variants = [] + + # TODO: These changes to quantity/max_quantity trigger another cart update, which + # is unnecessary. + for li in @line_items_present() if !stockLevels[li.variant.id]? - alert "Variant out of stock: #{li.variant.id}" li.quantity = 0 li.max_quantity = 0 if li.max_quantity? li.variant.count_on_hand = 0 + scope.variants.push li.variant else if stockLevels[li.variant.id].quantity < li.quantity - alert "Variant quantity reduced: #{li.variant.id}" li.quantity = stockLevels[li.variant.id].quantity + scope.variants.push li.variant if stockLevels[li.variant.id].max_quantity < li.max_quantity - alert "Variant max_quantity reduced: #{li.variant.id}" li.max_quantity = stockLevels[li.variant.id].max_quantity + scope.variants.push li.variant li.variant.count_on_hand = stockLevels[li.variant.id].on_hand + if scope.variants.length > 0 + $modal.open(templateUrl: "out_of_stock.html", scope: scope, windowClass: 'out-of-stock-modal') + popQueue: => @update_enqueued = false @scheduleUpdate() diff --git a/app/assets/javascripts/templates/out_of_stock.html.haml b/app/assets/javascripts/templates/out_of_stock.html.haml new file mode 100644 index 0000000000..72244d7ab6 --- /dev/null +++ b/app/assets/javascripts/templates/out_of_stock.html.haml @@ -0,0 +1,10 @@ +%h3 Reduced stock available + +%p While you've been shopping, the stock levels for one or more of the products in your cart have reduced. Here's what's changed: + +%p{'ng-repeat' => "v in variants"} + %em {{ v.name_to_display }} - {{ v.unit_to_display }} + %span{'ng-if' => "v.count_on_hand == 0"} + is now out of stock. + %span{'ng-if' => "v.count_on_hand > 0"} + now only has {{ v.count_on_hand }} remaining. diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index 4c2ab8cb63..836b77b271 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -51,12 +51,11 @@ Spree::OrdersController.class_eval do [li.variant.id, {quantity: li.quantity, max_quantity: li.max_quantity, - on_hand: li.variant.on_hand}] + on_hand: wrap_json_infinity(li.variant.on_hand)}] end ] end - def update_distribution @order = current_order(true) @@ -135,4 +134,9 @@ Spree::OrdersController.class_eval do end end + # Rails to_json encodes Float::INFINITY as Infinity, which is not valid JSON + # Return it as a large integer (max 32 bit signed int) + def wrap_json_infinity(n) + n == Float::INFINITY ? 2147483647 : n + end end diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index 756be1d3ec..df7be3508d 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -69,6 +69,14 @@ describe Spree::OrdersController do it "returns a hash with variant id, quantity, max_quantity and stock on hand" do controller.stock_levels.should == {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} end + + describe "encoding Infinity" do + let!(:v) { create(:variant, on_demand: true, count_on_hand: 0) } + + it "encodes Infinity as a large, finite integer" do + controller.stock_levels.should == {v.id => {quantity: 2, max_quantity: 3, on_hand: 2147483647}} + end + end end end diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index c252a3f041..b0d5a42127 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -243,14 +243,13 @@ feature "As a consumer I want to shop with a distributor", js: true do variant.update_attributes! on_hand: 0 # -- Messaging - alert_message = accept_alert do - fill_in "variants[#{variant.id}]", with: '1' - wait_until { !cart_dirty } - end + fill_in "variants[#{variant.id}]", with: '1' + wait_until { !cart_dirty } - # TODO: This will be a modal - expect(alert_message).to be - puts alert_message + within(".out-of-stock-modal") do + page.should have_content "stock levels for one or more of the products in your cart have reduced" + page.should have_content "#{product.name} - #{variant.unit_to_display} is now out of stock." + end # -- Page updates # Update amount in cart @@ -274,14 +273,13 @@ feature "As a consumer I want to shop with a distributor", js: true do variant.update_attributes! on_hand: 0 # -- Messaging - alert_message = accept_alert do - fill_in "variant_attributes[#{variant.id}][max_quantity]", with: '1' - wait_until { !cart_dirty } - end + fill_in "variant_attributes[#{variant.id}][max_quantity]", with: '1' + wait_until { !cart_dirty } - # TODO: This will be a modal - expect(alert_message).to be - puts alert_message + within(".out-of-stock-modal") do + page.should have_content "stock levels for one or more of the products in your cart have reduced" + page.should have_content "#{product.name} - #{variant.unit_to_display} is now out of stock." + end # -- Page updates # Update amount in cart From dac90c8003d5599d592f17f9dbc5a80f146827ed Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 11 Apr 2016 08:42:03 +1000 Subject: [PATCH 16/27] Fix specs --- spec/controllers/checkout_controller_spec.rb | 4 ++-- spec/features/consumer/shopping/checkout_auth_spec.rb | 11 +++++------ spec/features/consumer/shopping/shopping_spec.rb | 2 +- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/spec/controllers/checkout_controller_spec.rb b/spec/controllers/checkout_controller_spec.rb index ddacd0fdeb..1f5e40a841 100644 --- a/spec/controllers/checkout_controller_spec.rb +++ b/spec/controllers/checkout_controller_spec.rb @@ -34,13 +34,13 @@ describe CheckoutController do flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later." end - it "redirects to the shop when no line items are present" do + it "redirects to the cart when some items are out of stock" do controller.stub(:current_distributor).and_return(distributor) controller.stub(:current_order_cycle).and_return(order_cycle) controller.stub(:current_order).and_return(order) order.stub_chain(:insufficient_stock_lines, :present?).and_return true get :edit - response.should redirect_to shop_path + response.should redirect_to spree.cart_path end it "renders when both distributor and order cycle is selected" do diff --git a/spec/features/consumer/shopping/checkout_auth_spec.rb b/spec/features/consumer/shopping/checkout_auth_spec.rb index a19776f7a9..0b0683def0 100644 --- a/spec/features/consumer/shopping/checkout_auth_spec.rb +++ b/spec/features/consumer/shopping/checkout_auth_spec.rb @@ -9,7 +9,7 @@ feature "As a consumer I want to check out my cart", js: true do let(:distributor) { create(:distributor_enterprise, with_payment_and_shipping: true) } let(:supplier) { create(:supplier_enterprise) } - let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.master]) } + let!(:order_cycle) { create(:simple_order_cycle, distributors: [distributor], coordinator: create(:distributor_enterprise), variants: [product.variants.first]) } let(:product) { create(:simple_product, supplier: supplier) } let(:order) { create(:order, order_cycle: order_cycle, distributor: distributor) } let(:address) { create(:address, firstname: "Foo", lastname: "Bar") } @@ -23,7 +23,7 @@ feature "As a consumer I want to check out my cart", js: true do it "does not not render the login form when logged in" do quick_login_as user - visit checkout_path + visit checkout_path within "section[role='main']" do page.should_not have_content "Login" page.should have_checkout_details @@ -31,7 +31,7 @@ feature "As a consumer I want to check out my cart", js: true do end it "renders the login buttons when logged out" do - visit checkout_path + visit checkout_path within "section[role='main']" do page.should have_content "Login" click_button "Login" @@ -53,9 +53,8 @@ feature "As a consumer I want to check out my cart", js: true do end it "allows user to checkout as guest" do - visit checkout_path + visit checkout_path checkout_as_guest - page.should have_checkout_details + page.should have_checkout_details end end - diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index b0d5a42127..41a68dfb9b 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -318,7 +318,7 @@ feature "As a consumer I want to shop with a distributor", js: true do let(:variant) { create(:variant, product: product) } before do - add_product_and_variant_to_order_cycle(exchange, product, variant) + add_variant_to_order_cycle(exchange, variant) set_order_cycle(order, oc1) distributor.require_login = true distributor.save! From b2d78e7df6f1cb50913ece0fab5a7db5cb48d913 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Mon, 11 Apr 2016 08:55:05 +1000 Subject: [PATCH 17/27] Set allow_backorders explicitly for consistency in CI --- spec/models/spree/order_populator_spec.rb | 33 +++++++++++++---------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/spec/models/spree/order_populator_spec.rb b/spec/models/spree/order_populator_spec.rb index 9f7cc47c51..4a50e59fa8 100644 --- a/spec/models/spree/order_populator_spec.rb +++ b/spec/models/spree/order_populator_spec.rb @@ -194,23 +194,28 @@ module Spree describe "quantities_to_add" do let(:v) { double(:variant, on_hand: 10) } - context "when max_quantity is not provided" do - it "returns full amount when available" do - op.quantities_to_add(v, 5, nil).should == [5, nil] + + context "when backorders are not allowed" do + before { Spree::Config.allow_backorders = false } + + context "when max_quantity is not provided" do + it "returns full amount when available" do + op.quantities_to_add(v, 5, nil).should == [5, nil] + end + + it "returns a limited amount when not entirely available" do + op.quantities_to_add(v, 15, nil).should == [10, nil] + end end - it "returns a limited amount when not entirely available" do - op.quantities_to_add(v, 15, nil).should == [10, nil] - end - end + context "when max_quantity is provided" do + it "returns full amount when available" do + op.quantities_to_add(v, 5, 6).should == [5, 6] + end - context "when max_quantity is provided" do - it "returns full amount when available" do - op.quantities_to_add(v, 5, 6).should == [5, 6] - end - - it "returns a limited amount when not entirely available" do - op.quantities_to_add(v, 15, 16).should == [10, 10] + it "returns a limited amount when not entirely available" do + op.quantities_to_add(v, 15, 16).should == [10, 10] + end end end From 9b3139dba9d2d71a7b09ec99afd24590933c31aa Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Tue, 12 Apr 2016 11:18:03 +1000 Subject: [PATCH 18/27] When there's an out of stock product in the cart, visiting the shopfront returns user to the cart --- app/controllers/enterprises_controller.rb | 12 +++++++++- .../enterprises_controller_spec.rb | 24 ++++++++++++++++++- spec/controllers/shop_controller_spec.rb | 11 --------- 3 files changed, 34 insertions(+), 13 deletions(-) diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 8a875c76bf..9459cf43e3 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -5,6 +5,8 @@ class EnterprisesController < BaseController # These prepended filters are in the reverse order of execution prepend_before_filter :set_order_cycles, :require_distributor_chosen, :reset_order, only: :shop + before_filter :check_stock_levels, only: :shop + before_filter :clean_permalink, only: :check_permalink respond_to :js, only: :permalink_checker @@ -21,17 +23,25 @@ class EnterprisesController < BaseController end end + private def clean_permalink params[:permalink] = params[:permalink].parameterize end + def check_stock_levels + if current_order(true).insufficient_stock_lines.present? + flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) + redirect_to spree.cart_path + end + end + def reset_order distributor = Enterprise.is_distributor.find_by_permalink(params[:id]) || Enterprise.is_distributor.find(params[:id]) order = current_order(true) - if order.distributor and order.distributor != distributor + if order.distributor && order.distributor != distributor order.empty! order.set_order_cycle! nil end diff --git a/spec/controllers/enterprises_controller_spec.rb b/spec/controllers/enterprises_controller_spec.rb index b3cb6b5e32..0727488841 100644 --- a/spec/controllers/enterprises_controller_spec.rb +++ b/spec/controllers/enterprises_controller_spec.rb @@ -2,13 +2,14 @@ require 'spec_helper' describe EnterprisesController do describe "shopping for a distributor" do + let(:order) { controller.current_order(true) } before(:each) do @current_distributor = create(:distributor_enterprise, with_payment_and_shipping: true) @distributor = create(:distributor_enterprise, with_payment_and_shipping: true) @order_cycle1 = create(:simple_order_cycle, distributors: [@distributor], orders_open_at: 2.days.ago, orders_close_at: 3.days.from_now ) @order_cycle2 = create(:simple_order_cycle, distributors: [@distributor], orders_open_at: 3.days.ago, orders_close_at: 4.days.from_now ) - controller.current_order(true).distributor = @current_distributor + order.set_distributor! @current_distributor end it "sets the shop as the distributor on the order when shopping for the distributor" do @@ -52,6 +53,27 @@ describe EnterprisesController do controller.current_order.line_items.size.should == 1 end + describe "when an out of stock item is in the cart" do + let(:variant) { create(:variant, on_demand: false, on_hand: 10) } + let(:line_item) { create(:line_item, variant: variant) } + let(:order_cycle) { create(:simple_order_cycle, distributors: [@distributor], variants: [variant]) } + + before do + order.set_distribution! @current_distributor, order_cycle + order.line_items << line_item + + Spree::Config.set allow_backorders: false + variant.on_hand = 0 + variant.save! + end + + it "redirects to the cart" do + spree_get :shop, {id: @current_distributor} + + response.should redirect_to spree.cart_path + end + end + it "sets order cycle if only one is available at the chosen distributor" do @order_cycle2.destroy diff --git a/spec/controllers/shop_controller_spec.rb b/spec/controllers/shop_controller_spec.rb index 73231bc86c..a4ddab3ad3 100644 --- a/spec/controllers/shop_controller_spec.rb +++ b/spec/controllers/shop_controller_spec.rb @@ -69,17 +69,6 @@ describe ShopController do end - describe "producers/suppliers" do - let(:supplier) { create(:supplier_enterprise) } - let(:product) { create(:product, supplier: supplier) } - let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) } - - before do - exchange = order_cycle.exchanges.to_enterprises(distributor).outgoing.first - exchange.variants << product.master - end - end - describe "returning products" do let(:order_cycle) { create(:simple_order_cycle, distributors: [distributor]) } let(:exchange) { order_cycle.exchanges.to_enterprises(distributor).outgoing.first } From 3dcfa810fdec28cc5f9e81886327ba3e68d3f5f1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 14 Apr 2016 11:14:16 +1000 Subject: [PATCH 19/27] Display out of stock banner when viewing cart directly --- app/controllers/checkout_controller.rb | 2 -- app/controllers/enterprises_controller.rb | 1 - .../spree/orders_controller_decorator.rb | 12 +++++++--- .../spree/orders_controller_spec.rb | 22 ++++++++++++++++++- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/app/controllers/checkout_controller.rb b/app/controllers/checkout_controller.rb index 7b34bbcff5..c6d1291477 100644 --- a/app/controllers/checkout_controller.rb +++ b/app/controllers/checkout_controller.rb @@ -153,12 +153,10 @@ class CheckoutController < Spree::CheckoutController def raise_insufficient_quantity respond_to do |format| format.html do - flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) redirect_to cart_path end format.json do - flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) render json: {path: cart_path}, status: 400 end end diff --git a/app/controllers/enterprises_controller.rb b/app/controllers/enterprises_controller.rb index 9459cf43e3..fe2eda8bfd 100644 --- a/app/controllers/enterprises_controller.rb +++ b/app/controllers/enterprises_controller.rb @@ -32,7 +32,6 @@ class EnterprisesController < BaseController def check_stock_levels if current_order(true).insufficient_stock_lines.present? - flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) redirect_to spree.cart_path end end diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index 836b77b271..1b542f0829 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -1,9 +1,9 @@ require 'spree/core/controller_helpers/order_decorator' Spree::OrdersController.class_eval do - after_filter :populate_variant_attributes, :only => :populate - before_filter :update_distribution, :only => :update - before_filter :filter_order_params, :only => :update + after_filter :populate_variant_attributes, only: :populate + before_filter :update_distribution, only: :update + before_filter :filter_order_params, only: :update prepend_before_filter :require_order_cycle, only: :edit prepend_before_filter :require_distributor_chosen, only: :edit @@ -12,13 +12,19 @@ Spree::OrdersController.class_eval do include OrderCyclesHelper layout 'darkswarm' + # Patching to redirect to shop if order is empty def edit @order = current_order(true) + if @order.line_items.empty? redirect_to main_app.shop_path else associate_user + + if @order.insufficient_stock_lines.present? + flash[:error] = t(:spree_inventory_error_flash_for_insufficient_quantity) + end end end diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index df7be3508d..6baa633378 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -42,6 +42,26 @@ describe Spree::OrdersController do flash[:info].should == "The hub you have selected is temporarily closed for orders. Please try again later." end + describe "when an item has insufficient stock" do + let(:order) { subject.current_order(true) } + let(:oc) { create(:simple_order_cycle, distributors: [d], variants: [variant]) } + let(:d) { create(:distributor_enterprise, shipping_methods: [create(:shipping_method)], payment_methods: [create(:payment_method)]) } + let(:variant) { create(:variant, on_demand: false, on_hand: 5) } + let(:line_item) { order.line_items.last } + + before do + order.set_distribution! d, oc + order.add_variant variant, 5 + variant.update_attributes! on_hand: 3 + end + + it "displays a flash message when we view the cart" do + spree_get :edit + expect(response.status).to eq 200 + flash[:error].should == "An item in your cart has become unavailable." + end + end + describe "returning stock levels in JSON on success" do let(:product) { create(:simple_product) } @@ -120,7 +140,7 @@ describe Spree::OrdersController do describe "when I pass params that includes a line item no longer in our cart" do it "should silently ignore the missing line item" do order = subject.current_order(true) - li = order.add_variant(create(:simple_product, on_hand: 110).master) + li = order.add_variant(create(:simple_product, on_hand: 110).variants.first) spree_get :update, order: { line_items_attributes: { "0" => {quantity: "0", id: "9999"}, "1" => {quantity: "99", id: li.id} From 5151779f805b33b6d094b16297788689e88ddce0 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 14 Apr 2016 15:20:44 +1000 Subject: [PATCH 20/27] When update is for another line item, still update all stock levels and show warnings --- .../spree/orders_controller_decorator.rb | 1 + app/models/spree/line_item_decorator.rb | 10 ++++++ app/models/spree/order_decorator.rb | 5 +++ .../consumer/shopping/shopping_spec.rb | 36 +++++++++++++++++++ spec/models/spree/line_item_spec.rb | 35 ++++++++++++++++++ 5 files changed, 87 insertions(+) diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index 1b542f0829..f9c7f53001 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -41,6 +41,7 @@ Spree::OrdersController.class_eval do fire_event('spree.cart.add') fire_event('spree.order.contents_changed') + current_order.cap_quantity_at_stock! current_order.update! render json: {error: false, stock_levels: stock_levels}, status: 200 diff --git a/app/models/spree/line_item_decorator.rb b/app/models/spree/line_item_decorator.rb index 9db9ae1269..b9f8af577f 100644 --- a/app/models/spree/line_item_decorator.rb +++ b/app/models/spree/line_item_decorator.rb @@ -43,6 +43,16 @@ Spree::LineItem.class_eval do where('spree_adjustments.id IS NULL') + def cap_quantity_at_stock! + attrs = {} + + attrs[:quantity] = variant.on_hand if quantity > variant.on_hand + attrs[:max_quantity] = variant.on_hand if (max_quantity || 0) > variant.on_hand + + update_attributes!(attrs) if attrs.any? + end + + def has_tax? adjustments.included_tax.any? end diff --git a/app/models/spree/order_decorator.rb b/app/models/spree/order_decorator.rb index 53a1b81873..a06a34bcd0 100644 --- a/app/models/spree/order_decorator.rb +++ b/app/models/spree/order_decorator.rb @@ -144,6 +144,11 @@ Spree::Order.class_eval do current_item end + def cap_quantity_at_stock! + line_items.each &:cap_quantity_at_stock! + end + + def set_distributor!(distributor) self.distributor = distributor self.order_cycle = nil unless self.order_cycle.andand.has_distributor? distributor diff --git a/spec/features/consumer/shopping/shopping_spec.rb b/spec/features/consumer/shopping/shopping_spec.rb index 41a68dfb9b..9dc0a39dac 100644 --- a/spec/features/consumer/shopping/shopping_spec.rb +++ b/spec/features/consumer/shopping/shopping_spec.rb @@ -292,6 +292,42 @@ feature "As a consumer I want to shop with a distributor", js: true do page.should have_selector "#variants_#{variant.id}_max[disabled='disabled']" end end + + context "when the update is for another product" do + it "updates quantity" do + fill_in "variants[#{variant.id}]", with: '1' + wait_until { !cart_dirty } + + variant.update_attributes! on_hand: 0 + + fill_in "variants[#{variant2.id}]", with: '1' + wait_until { !cart_dirty } + + within(".out-of-stock-modal") do + page.should have_content "stock levels for one or more of the products in your cart have reduced" + page.should have_content "#{product.name} - #{variant.unit_to_display} is now out of stock." + end + end + + context "group buy products" do + let(:product) { create(:simple_product, group_buy: true) } + + it "updates max_quantity" do + fill_in "variants[#{variant.id}]", with: '1' + fill_in "variant_attributes[#{variant.id}][max_quantity]", with: '2' + wait_until { !cart_dirty } + variant.update_attributes! on_hand: 1 + + fill_in "variants[#{variant2.id}]", with: '1' + wait_until { !cart_dirty } + + within(".out-of-stock-modal") do + page.should have_content "stock levels for one or more of the products in your cart have reduced" + page.should have_content "#{product.name} - #{variant.unit_to_display} now only has 1 remaining" + end + end + end + end end end diff --git a/spec/models/spree/line_item_spec.rb b/spec/models/spree/line_item_spec.rb index 6bb18fafa3..2b5362e94f 100644 --- a/spec/models/spree/line_item_spec.rb +++ b/spec/models/spree/line_item_spec.rb @@ -42,6 +42,41 @@ module Spree end end + describe "capping quantity at stock level" do + let!(:v) { create(:variant, on_demand: false, on_hand: 10) } + let!(:li) { create(:line_item, variant: v, quantity: 10, max_quantity: 10) } + + before do + v.update_attributes! on_hand: 5 + end + + it "caps quantity" do + li.cap_quantity_at_stock! + li.reload.quantity.should == 5 + end + + it "caps max_quantity" do + li.cap_quantity_at_stock! + li.reload.max_quantity.should == 5 + end + + it "works for products without max_quantity" do + li.update_column :max_quantity, nil + li.cap_quantity_at_stock! + li.reload + li.quantity.should == 5 + li.max_quantity.should be_nil + end + + it "does nothing for on_demand items" do + v.update_attributes! on_demand: true + li.cap_quantity_at_stock! + li.reload + li.quantity.should == 10 + li.max_quantity.should == 10 + end + end + describe "calculating price with adjustments" do it "does not return fractional cents" do li = LineItem.new From 06d7665bf97fda4f4ad9a71b1c0bfba5360f2949 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 Apr 2016 09:54:56 +1000 Subject: [PATCH 21/27] Prospective fix for intermittent spec fail --- spec/controllers/spree/orders_controller_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index 6baa633378..c0a844a4bc 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -50,6 +50,7 @@ describe Spree::OrdersController do let(:line_item) { order.line_items.last } before do + Spree::Config.allow_backorders = false order.set_distribution! d, oc order.add_variant variant, 5 variant.update_attributes! on_hand: 3 From 6cba935a6561e0afdc7b66099939c8f020bca13f Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 Apr 2016 11:26:30 +1000 Subject: [PATCH 22/27] Add close button to out of stock modal --- app/assets/javascripts/templates/out_of_stock.html.haml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/assets/javascripts/templates/out_of_stock.html.haml b/app/assets/javascripts/templates/out_of_stock.html.haml index 72244d7ab6..d894583fcf 100644 --- a/app/assets/javascripts/templates/out_of_stock.html.haml +++ b/app/assets/javascripts/templates/out_of_stock.html.haml @@ -1,3 +1,6 @@ +%a.close-reveal-modal{"ng-click" => "$close()"} + %i.ofn-i_009-close + %h3 Reduced stock available %p While you've been shopping, the stock levels for one or more of the products in your cart have reduced. Here's what's changed: From 779be7c5a0657d1fccba5cf35dfe197a4f3843e1 Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Thu, 21 Apr 2016 16:37:15 +1000 Subject: [PATCH 23/27] Extract params parsing into single method --- app/models/spree/order_populator_decorator.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/app/models/spree/order_populator_decorator.rb b/app/models/spree/order_populator_decorator.rb index 82d9aa85e7..38c908a530 100644 --- a/app/models/spree/order_populator_decorator.rb +++ b/app/models/spree/order_populator_decorator.rb @@ -11,8 +11,7 @@ Spree::OrderPopulator.class_eval do if valid? @order.with_lock do - variants = read_products_hash(from_hash) + - read_variants_hash(from_hash) + variants = read_variants from_hash variants.each do |v| if varies_from_cart(v) @@ -31,6 +30,11 @@ Spree::OrderPopulator.class_eval do valid? end + def read_variants(data) + read_products_hash(data) + + read_variants_hash(data) + end + def read_products_hash(data) (data[:products] || []).map do |product_id, variant_id| {variant_id: variant_id, quantity: data[:quantity]} From 28d40bf27d3418d3e8b89bec03e65a83887be3a7 Mon Sep 17 00:00:00 2001 From: Rob Harrington Date: Thu, 21 Apr 2016 21:27:52 +1000 Subject: [PATCH 24/27] Fixing font styling on enterprise name in shop product summary --- app/views/shop/products/_summary.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/shop/products/_summary.html.haml b/app/views/shop/products/_summary.html.haml index 6d36ac81ce..8671b00cbf 100644 --- a/app/views/shop/products/_summary.html.haml +++ b/app/views/shop/products/_summary.html.haml @@ -13,8 +13,9 @@ %em = t :products_from %span - %enterprise-modal - %i.ofn-i_036-producers{"bo-text" => "enterprise.name"} + %enterprise-modal + %i.ofn-i_036-producers + %span{"bo-bind" => "enterprise.name"} .small-2.medium-2.large-1.columns.text-center .taxon-flag %render-svg{path: "{{product.primary_taxon.icon}}"} From a26266159cefcdc55c2e1bff7d5498a9d88c069d Mon Sep 17 00:00:00 2001 From: Rohan Mitchell Date: Fri, 22 Apr 2016 10:47:20 +1000 Subject: [PATCH 25/27] Fix timing issue: change in client-side value during server update --- .../darkswarm/services/cart.js.coffee | 20 ++++----- .../spree/orders_controller_decorator.rb | 26 ++++++++++-- app/models/spree/order_populator_decorator.rb | 6 ++- .../spree/orders_controller_spec.rb | 30 ++++++++++++-- .../darkswarm/services/cart_spec.js.coffee | 41 +++++++++++++++++-- 5 files changed, 100 insertions(+), 23 deletions(-) diff --git a/app/assets/javascripts/darkswarm/services/cart.js.coffee b/app/assets/javascripts/darkswarm/services/cart.js.coffee index 4f808e0710..d75ef7a628 100644 --- a/app/assets/javascripts/darkswarm/services/cart.js.coffee +++ b/app/assets/javascripts/darkswarm/services/cart.js.coffee @@ -28,6 +28,7 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $roo update: => @update_running = true + $http.post('/orders/populate', @data()).success (data, status)=> @saved() @update_running = false @@ -48,19 +49,14 @@ Darkswarm.factory 'Cart', (CurrentOrder, Variants, $timeout, $http, $modal, $roo # is unnecessary. for li in @line_items_present() - if !stockLevels[li.variant.id]? - li.quantity = 0 - li.max_quantity = 0 if li.max_quantity? - li.variant.count_on_hand = 0 - scope.variants.push li.variant - else - if stockLevels[li.variant.id].quantity < li.quantity - li.quantity = stockLevels[li.variant.id].quantity - scope.variants.push li.variant - if stockLevels[li.variant.id].max_quantity < li.max_quantity - li.max_quantity = stockLevels[li.variant.id].max_quantity - scope.variants.push li.variant + if stockLevels[li.variant.id]? li.variant.count_on_hand = stockLevels[li.variant.id].on_hand + if li.quantity > li.variant.count_on_hand + li.quantity = li.variant.count_on_hand + scope.variants.push li.variant + if li.max_quantity > li.variant.count_on_hand + li.max_quantity = li.variant.count_on_hand + scope.variants.push(li.variant) unless li.variant in scope.variants if scope.variants.length > 0 $modal.open(templateUrl: "out_of_stock.html", scope: scope, windowClass: 'out-of-stock-modal') diff --git a/app/controllers/spree/orders_controller_decorator.rb b/app/controllers/spree/orders_controller_decorator.rb index f9c7f53001..e675ce7e94 100644 --- a/app/controllers/spree/orders_controller_decorator.rb +++ b/app/controllers/spree/orders_controller_decorator.rb @@ -44,7 +44,10 @@ Spree::OrdersController.class_eval do current_order.cap_quantity_at_stock! current_order.update! - render json: {error: false, stock_levels: stock_levels}, status: 200 + variant_ids = variant_ids_in(populator.variants_h) + + render json: {error: false, stock_levels: stock_levels(current_order, variant_ids)}, + status: 200 else render json: {error: true}, status: 412 @@ -52,9 +55,26 @@ Spree::OrdersController.class_eval do end end - def stock_levels + # Report the stock levels in the order for all variant ids requested + def stock_levels(order, variant_ids) + stock_levels = li_stock_levels(order) + + li_variant_ids = stock_levels.keys + (variant_ids - li_variant_ids).each do |variant_id| + stock_levels[variant_id] = {quantity: 0, max_quantity: 0, + on_hand: Spree::Variant.find(variant_id).on_hand} + end + + stock_levels + end + + def variant_ids_in(variants_h) + variants_h.map { |v| v[:variant_id].to_i } + end + + def li_stock_levels(order) Hash[ - current_order.line_items.map do |li| + order.line_items.map do |li| [li.variant.id, {quantity: li.quantity, max_quantity: li.max_quantity, diff --git a/app/models/spree/order_populator_decorator.rb b/app/models/spree/order_populator_decorator.rb index 38c908a530..a106de6936 100644 --- a/app/models/spree/order_populator_decorator.rb +++ b/app/models/spree/order_populator_decorator.rb @@ -1,6 +1,8 @@ require 'open_food_network/scope_variant_to_hub' Spree::OrderPopulator.class_eval do + attr_reader :variants_h + def populate(from_hash, overwrite = false) @distributor, @order_cycle = distributor_and_order_cycle # Refactor: We may not need this validation - we can't change distribution here, so @@ -31,8 +33,8 @@ Spree::OrderPopulator.class_eval do end def read_variants(data) - read_products_hash(data) + - read_variants_hash(data) + @variants_h = read_products_hash(data) + + read_variants_hash(data) end def read_products_hash(data) diff --git a/spec/controllers/spree/orders_controller_spec.rb b/spec/controllers/spree/orders_controller_spec.rb index c0a844a4bc..286bb9217a 100644 --- a/spec/controllers/spree/orders_controller_spec.rb +++ b/spec/controllers/spree/orders_controller_spec.rb @@ -67,9 +67,11 @@ describe Spree::OrdersController do let(:product) { create(:simple_product) } it "returns stock levels as JSON" do + controller.stub(:variant_ids_in) { [123] } controller.stub(:stock_levels) { 'my_stock_levels' } Spree::OrderPopulator.stub(:new).and_return(populator = double()) populator.stub(:populate) { true } + populator.stub(:variants_h) { {} } xhr :post, :populate, use_route: :spree, format: :json @@ -81,6 +83,7 @@ describe Spree::OrdersController do let!(:order) { create(:order) } let!(:li) { create(:line_item, order: order, variant: v, quantity: 2, max_quantity: 3) } let!(:v) { create(:variant, count_on_hand: 4) } + let!(:v2) { create(:variant, count_on_hand: 2) } before do order.reload @@ -88,17 +91,37 @@ describe Spree::OrdersController do end it "returns a hash with variant id, quantity, max_quantity and stock on hand" do - controller.stock_levels.should == {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} + controller.stock_levels(order, [v.id]).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} + end + + it "includes all line items, even when the variant_id is not specified" do + controller.stock_levels(order, []).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}} + end + + it "includes an empty quantity entry for variants that aren't in the order" do + controller.stock_levels(order, [v.id, v2.id]).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 4}, + v2.id => {quantity: 0, max_quantity: 0, on_hand: 2}} end describe "encoding Infinity" do let!(:v) { create(:variant, on_demand: true, count_on_hand: 0) } it "encodes Infinity as a large, finite integer" do - controller.stock_levels.should == {v.id => {quantity: 2, max_quantity: 3, on_hand: 2147483647}} + controller.stock_levels(order, [v.id]).should == + {v.id => {quantity: 2, max_quantity: 3, on_hand: 2147483647}} end end end + + it "extracts variant ids from the populator" do + variants_h = [{:variant_id=>"900", :quantity=>2, :max_quantity=>nil}, + {:variant_id=>"940", :quantity=>3, :max_quantity=>3}] + + controller.variant_ids_in(variants_h).should == [900, 940] + end end context "adding a group buy product to the cart" do @@ -118,7 +141,8 @@ describe Spree::OrdersController do it "returns HTTP success when successful" do Spree::OrderPopulator.stub(:new).and_return(populator = double()) - populator.stub(:populate).and_return true + populator.stub(:populate) { true } + populator.stub(:variants_h) { {} } xhr :post, :populate, use_route: :spree, format: :json response.status.should == 200 end diff --git a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee index 58b7795124..6df74e3ede 100644 --- a/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee +++ b/spec/javascripts/unit/darkswarm/services/cart_spec.js.coffee @@ -130,21 +130,24 @@ describe 'Cart service', -> describe "when an item is out of stock", -> it "reduces the quantity in the cart", -> li = {variant: {id: 1}, quantity: 5} + stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}} spyOn(Cart, 'line_items_present').andReturn [li] - Cart.compareAndNotifyStockLevels {} + Cart.compareAndNotifyStockLevels stockLevels expect(li.quantity).toEqual 0 expect(li.max_quantity).toBeUndefined() it "reduces the max_quantity in the cart", -> li = {variant: {id: 1}, quantity: 5, max_quantity: 6} + stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}} spyOn(Cart, 'line_items_present').andReturn [li] - Cart.compareAndNotifyStockLevels {} + Cart.compareAndNotifyStockLevels stockLevels expect(li.max_quantity).toEqual 0 it "resets the count on hand available", -> li = {variant: {id: 1, count_on_hand: 10}, quantity: 5} + stockLevels = {1: {quantity: 0, max_quantity: 0, on_hand: 0}} spyOn(Cart, 'line_items_present').andReturn [li] - Cart.compareAndNotifyStockLevels {} + Cart.compareAndNotifyStockLevels stockLevels expect(li.variant.count_on_hand).toEqual 0 describe "when the quantity available is less than that requested", -> @@ -170,6 +173,38 @@ describe 'Cart service', -> Cart.compareAndNotifyStockLevels stockLevels expect(li.variant.count_on_hand).toEqual 6 + describe "when the client-side quantity has been increased during the request", -> + it "does not reset the quantity", -> + li = {variant: {id: 1}, quantity: 6} + stockLevels = {1: {quantity: 5, on_hand: 6}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.quantity).toEqual 6 + expect(li.max_quantity).toBeUndefined() + + it "does not reset the max_quantity", -> + li = {variant: {id: 1}, quantity: 5, max_quantity: 7} + stockLevels = {1: {quantity: 5, max_quantity: 6, on_hand: 7}} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels stockLevels + expect(li.quantity).toEqual 5 + expect(li.max_quantity).toEqual 7 + + describe "when the client-side quantity has been changed from 0 to 1 during the request", -> + it "does not reset the quantity", -> + li = {variant: {id: 1}, quantity: 1} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels {} + expect(li.quantity).toEqual 1 + expect(li.max_quantity).toBeUndefined() + + it "does not reset the max_quantity", -> + li = {variant: {id: 1}, quantity: 1, max_quantity: 1} + spyOn(Cart, 'line_items_present').andReturn [li] + Cart.compareAndNotifyStockLevels {} + expect(li.quantity).toEqual 1 + expect(li.max_quantity).toEqual 1 + it "pops the queue", -> Cart.update_enqueued = true spyOn(Cart, 'scheduleUpdate') From 23e598f2f8639e8635892cee1bbd360d43bdc675 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Wed, 20 Apr 2016 14:45:22 +1000 Subject: [PATCH 26/27] Destroy customer without flash notice --- app/controllers/admin/customers_controller.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/app/controllers/admin/customers_controller.rb b/app/controllers/admin/customers_controller.rb index bd865c8130..24c26661e5 100644 --- a/app/controllers/admin/customers_controller.rb +++ b/app/controllers/admin/customers_controller.rb @@ -26,6 +26,23 @@ module Admin end end + # copy of Spree::Admin::ResourceController without flash notice + def destroy + invoke_callbacks(:destroy, :before) + if @object.destroy + invoke_callbacks(:destroy, :after) + respond_with(@object) do |format| + format.html { redirect_to location_after_destroy } + format.js { render partial: "spree/admin/shared/destroy" } + end + else + invoke_callbacks(:destroy, :fails) + respond_with(@object) do |format| + format.html { redirect_to location_after_destroy } + end + end + end + private def collection From 2367b73d3a037a9d7e7a8b981aa29c382a2353b0 Mon Sep 17 00:00:00 2001 From: Maikel Linke Date: Fri, 22 Apr 2016 14:53:08 +1000 Subject: [PATCH 27/27] Revert "Associate new users with existing customer records" This reverts commit a25f4fdf44cb218343a2df3ae94748be892b24bc. Since email addresses are not validated, these associations would allow an attacker to signup with the email address of another person and view their orders. --- app/models/spree/user_decorator.rb | 5 ----- spec/models/spree/user_spec.rb | 17 ----------------- 2 files changed, 22 deletions(-) diff --git a/app/models/spree/user_decorator.rb b/app/models/spree/user_decorator.rb index be60307d52..17473fd521 100644 --- a/app/models/spree/user_decorator.rb +++ b/app/models/spree/user_decorator.rb @@ -15,7 +15,6 @@ Spree.user_class.class_eval do accepts_nested_attributes_for :enterprise_roles, :allow_destroy => true attr_accessible :enterprise_ids, :enterprise_roles_attributes, :enterprise_limit - after_create :associate_customers after_create :send_signup_confirmation validate :limit_owned_enterprises @@ -42,10 +41,6 @@ Spree.user_class.class_eval do customers.of(enterprise).first end - def associate_customers - Customer.update_all({ user_id: id }, { user_id: nil, email: email }) - end - def send_signup_confirmation Delayed::Job.enqueue ConfirmSignupJob.new(id) end diff --git a/spec/models/spree/user_spec.rb b/spec/models/spree/user_spec.rb index 923bc93183..c7d284b143 100644 --- a/spec/models/spree/user_spec.rb +++ b/spec/models/spree/user_spec.rb @@ -53,23 +53,6 @@ describe Spree.user_class do create(:user) end.to enqueue_job ConfirmSignupJob end - - it "should not create a customer" do - expect do - create(:user) - end.to change(Customer, :count).by(0) - end - - describe "when a customer record exists" do - let!(:customer) { create(:customer, user: nil) } - - it "should not create a customer" do - expect(customer.user).to be nil - user = create(:user, email: customer.email) - customer.reload - expect(customer.user).to eq user - end - end end describe "known_users" do