Merge pull request #13192 from dacook/display-customer-contacts-in-reports-13129

[FF] Add option to display customer contact details to suppliers in reports
This commit is contained in:
Filipe
2025-04-08 20:34:44 +01:00
committed by GitHub
19 changed files with 207 additions and 90 deletions

View File

@@ -9,6 +9,7 @@ module Api
:preferred_shopfront_message, :preferred_shopfront_closed_message,
:preferred_shopfront_taxon_order, :preferred_shopfront_producer_order,
:preferred_shopfront_order_cycle_order, :show_customer_names_to_suppliers,
:show_customer_contacts_to_suppliers,
:preferred_shopfront_product_sorting_method, :owner, :contact, :users, :tag_groups,
:default_tag_group, :require_login, :allow_guest_orders, :allow_order_changes,
:logo, :promo_image, :terms_and_conditions,

View File

@@ -1,5 +1,8 @@
# frozen_string_literal: true
# Mask user data from suppliers, unless explicitly allowed
# See also: lib/reporting/queries/mask_data.rb
#
module Orders
class MaskDataService
def initialize(order)
@@ -8,7 +11,8 @@ module Orders
def call
mask_customer_names unless customer_names_allowed?
mask_contact_data
mask_contact_data unless cutomer_contacts_allowed?
mask_address
end
private
@@ -20,18 +24,27 @@ module Orders
end
def mask_customer_names
order.bill_address&.assign_attributes(firstname: I18n.t('admin.reports.hidden'),
order.bill_address&.assign_attributes(firstname: I18n.t('admin.reports.hidden_field'),
lastname: "")
order.ship_address&.assign_attributes(firstname: I18n.t('admin.reports.hidden'),
order.ship_address&.assign_attributes(firstname: I18n.t('admin.reports.hidden_field'),
lastname: "")
end
def cutomer_contacts_allowed?
order.distributor.show_customer_contacts_to_suppliers
end
def mask_contact_data
order.bill_address&.assign_attributes(phone: "", address1: "", address2: "",
order.bill_address&.assign_attributes(phone: "")
order.ship_address&.assign_attributes(phone: "")
order.assign_attributes(email: I18n.t('admin.reports.hidden_field'))
end
def mask_address
order.bill_address&.assign_attributes(address1: "", address2: "",
city: "", zipcode: "", state: nil)
order.ship_address&.assign_attributes(phone: "", address1: "", address2: "",
order.ship_address&.assign_attributes(address1: "", address2: "",
city: "", zipcode: "", state: nil)
order.assign_attributes(email: I18n.t('admin.reports.hidden'))
end
end
end

View File

@@ -33,7 +33,8 @@ module PermittedAttributes
:preferred_product_selection_from_inventory_only, :preferred_shopfront_message,
:preferred_shopfront_closed_message, :preferred_shopfront_taxon_order,
:preferred_shopfront_producer_order, :preferred_shopfront_order_cycle_order,
:show_customer_names_to_suppliers, :preferred_shopfront_product_sorting_method,
:show_customer_names_to_suppliers, :show_customer_contacts_to_suppliers,
:preferred_shopfront_product_sorting_method,
:preferred_invoice_order_by_supplier,
:preferred_product_low_stock_display,
:hide_ofn_navigation, :white_label_logo, :white_label_logo_link,

View File

@@ -106,8 +106,20 @@
%div{'ofn-with-tip' => t('.customer_names_tip')}
%a= t 'admin.whats_this'
.three.columns
= radio_button :enterprise, :show_customer_names_to_suppliers, true
= label :enterprise_show_customer_names_to_suppliers, t('.customer_names_true'), value: :true
= f.radio_button :show_customer_names_to_suppliers, true
= f.label :show_customer_names_to_suppliers, t('.customer_names_true'), value: :true
.five.columns.omega
= radio_button :enterprise, :show_customer_names_to_suppliers, false
= label :enterprise_show_customer_names_to_suppliers, t('.customer_names_false'), value: :false
= f.radio_button :show_customer_names_to_suppliers, false
= f.label :show_customer_names_to_suppliers, t('.customer_names_false'), value: :false
.row
.three.columns.alpha
%label= t '.customer_contacts_in_reports'
%div{'ofn-with-tip' => t('.customer_contacts_tip')}
%a= t 'admin.whats_this'
.three.columns
= f.radio_button :show_customer_contacts_to_suppliers, true
= f.label :show_customer_contacts_to_suppliers, t('.customer_contacts_true'), value: :true
.five.columns.omega
= f.radio_button :show_customer_contacts_to_suppliers, false
= f.label :show_customer_contacts_to_suppliers, t('.customer_contacts_false'), value: :false

View File

@@ -1350,6 +1350,10 @@ en:
customer_names_tip: "Enable your suppliers to see your customers names in reports"
customer_names_false: "Disabled"
customer_names_true: "Enabled"
customer_contacts_in_reports: "Customer contact details in reports"
customer_contacts_tip: "Enable your suppliers to see your customer email and phone numbers in reports"
customer_contacts_false: "Disabled"
customer_contacts_true: "Enabled"
shopfront_message: "Shopfront Message"
shopfront_message_placeholder: >
An optional message to welcome customers and explain how to shop with you. If text is entered here it will be displayed in a home tab when customers first arrive at your shopfront.
@@ -1761,7 +1765,7 @@ en:
not_visible: "%{enterprise} is not visible and so cannot be found on the map or in searches"
reports:
deprecated: "This report is deprecated and will be removed in a future release."
hidden: HIDDEN
hidden_field: "< Hidden >"
unitsize: UNITSIZE
total: TOTAL
total_items: TOTAL ITEMS
@@ -1839,7 +1843,6 @@ en:
no_report_type: "Please specify a report type"
report_not_found: "Report not found"
missing_ransack_params: "Please supply Ransack search params in the request"
hidden_field: "< Hidden >"
summary_row:
total: "TOTAL"
table:
@@ -4652,8 +4655,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
reports:
table:
select_and_search: "Select filters and click on %{option} to access your data."
customer_names_message:
customer_names_tip: "If customer names are hidden for orders you have supplied, you can contact the distributor and ask if they can update their shop preferences to allow their suppliers to view customer names."
hidden_customer_details_tip: "If customer names and/or contact details are hidden, you can ask the distributor to update their shop preferences to allow their suppliers to view customer details in reports."
products_and_inventory:
all_products:
message: "Note that stock levels reported are from supplier product lists only. If you are using Inventory to manage your stock quantities these values will be ignored in this report."

View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
class AddShowCustomerContactsToSuppliersToEnterprises < ActiveRecord::Migration[7.0]
def change
add_column :enterprises, :show_customer_contacts_to_suppliers, :boolean, default: false,
null: false
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.0].define(version: 2025_01_28_031518) do
ActiveRecord::Schema[7.0].define(version: 2025_03_04_234657) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_stat_statements"
enable_extension "plpgsql"
@@ -230,6 +230,8 @@ ActiveRecord::Schema[7.0].define(version: 2025_01_28_031518) do
t.text "white_label_logo_link"
t.boolean "hide_groups_tab", default: false
t.string "external_billing_id", limit: 128
t.boolean "enable_producers_to_edit_orders", default: false, null: false
t.boolean "show_customer_contacts_to_suppliers", default: false, null: false
t.index ["address_id"], name: "index_enterprises_on_address_id"
t.index ["is_primary_producer", "sells"], name: "index_enterprises_on_is_primary_producer_and_sells"
t.index ["name"], name: "index_enterprises_on_name", unique: true

View File

@@ -0,0 +1,36 @@
# frozen_string_literal: true
# Mask user data from suppliers, unless explicitly allowed
# See also: app/services/orders/mask_data_service.rb
#
module Reporting
module Queries
module MaskData
include Tables
def mask_customer_name(field)
masked(field, managed_order_mask_rule(:show_customer_names_to_suppliers))
end
def mask_contact_data(field)
masked(field, managed_order_mask_rule(:show_customer_contacts_to_suppliers))
end
def masked(field, mask_rule = nil)
Arel::Nodes::Case.new.
when(mask_rule).
then(field).
else(quoted(I18n.t("hidden_field", scope: i18n_scope)))
end
private
# Show unmasked data if order is managed by user, or if distributor allows suppliers
def managed_order_mask_rule(condition_name)
id = raw("#{managed_orders_alias.name}.id") # rubocop:disable Rails/OutputSafety
line_item_table[:order_id].in(id).
or(distributor_alias[condition_name].eq(true))
end
end
end
end

View File

@@ -5,6 +5,7 @@ module Reporting
class QueryBuilder < QueryInterface
include Joins
include Tables
include MaskData
attr_reader :grouping_fields
@@ -49,13 +50,6 @@ module Reporting
reflect query.order(*instance_exec(&ordering_fields))
end
def masked(field, message = nil, mask_rule = nil)
Case.new.
when(mask_rule || default_mask_rule).
then(field).
else(quoted(message || I18n.t("hidden_field", scope: i18n_scope)))
end
def distinct_results(fields = nil)
return reflect query.distinct if fields.blank?
@@ -80,12 +74,6 @@ module Reporting
private
def default_mask_rule
id = raw("#{managed_orders_alias.name}.id") # rubocop:disable Rails/OutputSafety
line_item_table[:order_id].in(id).
or(distributor_alias[:show_customer_names_to_suppliers].eq(true))
end
def summary_row_title
I18n.t("total", scope: i18n_scope)
end

View File

@@ -5,7 +5,7 @@ module Reporting
module BulkCoop
class Base < ReportTemplate
def message
I18n.t("spree.admin.reports.customer_names_message.customer_names_tip")
I18n.t("spree.admin.reports.hidden_customer_details_tip")
end
def search

View File

@@ -23,7 +23,7 @@ module Reporting
end
def message
I18n.t("spree.admin.reports.customer_names_message.customer_names_tip")
I18n.t("spree.admin.reports.hidden_customer_details_tip")
end
def query_result

View File

@@ -5,7 +5,7 @@ module Reporting
module OrdersAndFulfillment
class Base < ReportTemplate
def message
I18n.t("spree.admin.reports.customer_names_message.customer_names_tip")
I18n.t("spree.admin.reports.hidden_customer_details_tip")
end
def default_params

View File

@@ -5,7 +5,7 @@ module Reporting
module Packing
class Base < ReportQueryTemplate
def message
I18n.t("spree.admin.reports.customer_names_message.customer_names_tip")
I18n.t("spree.admin.reports.hidden_customer_details_tip")
end
def report_query
@@ -42,10 +42,10 @@ module Reporting
lambda do
{
hub: distributor_alias[:name],
customer_code: masked(customer_table[:code]),
last_name: masked(bill_address_alias[:lastname]),
first_name: masked(bill_address_alias[:firstname]),
phone: masked(bill_address_alias[:phone]),
customer_code: mask_customer_name(customer_table[:code]),
last_name: mask_customer_name(bill_address_alias[:lastname]),
first_name: mask_customer_name(bill_address_alias[:firstname]),
phone: mask_contact_data(bill_address_alias[:phone]),
supplier: supplier_alias[:name],
product: product_table[:name],
variant: variant_full_name,

View File

@@ -133,7 +133,7 @@ module Reporting
it "shows line items supplied by my producers, with names hidden" do
expect(subject.table_items).to eq([li2])
expect(subject.table_items.first.order.bill_address.firstname).to eq("HIDDEN")
expect(subject.table_items.first.order.bill_address.firstname).to eq("< Hidden >")
end
end

View File

@@ -51,7 +51,7 @@ RSpec.describe Reporting::LineItems do
it 'returns masked data' do
line_items = reports_line_items.list
expect(line_items.first.order.email).to eq('HIDDEN')
expect(line_items.first.order.email).to eq("< Hidden >")
end
context "when filtering by product" do

View File

@@ -120,7 +120,7 @@ RSpec.describe Reporting::Reports::OrdersAndDistributors::Base do
it "shows line items supplied by my producers, with contact details hidden" do
expect(row).not_to include("FirstName LastName")
expect(row).not_to include("123-456", "City", order.email)
expect(row[2..5]).to eq ["HIDDEN", "HIDDEN", "", ""]
expect(row[2..5]).to eq ["< Hidden >", "< Hidden >", "", ""]
end
context "where the distributor allows suppliers to see customer names" do
@@ -131,7 +131,18 @@ RSpec.describe Reporting::Reports::OrdersAndDistributors::Base do
it "shows line items supplied by my producers, with only contact names shown" do
expect(row).to include("FirstName LastName")
expect(row).not_to include("123-456", "City", order.email)
expect(row[2..5]).to eq [bill_address.full_name, "HIDDEN", "", ""]
expect(row[2..5]).to eq [bill_address.full_name, "< Hidden >", "", ""]
end
end
context "where the distributor allows suppliers to see customer phone and email" do
before do
distributor.update_columns show_customer_contacts_to_suppliers: true
end
it "shows line items supplied by my producers, with only contact details shown" do
expect(row).not_to include("FirstName LastName", "City")
expect(row[2..5]).to eq ["< Hidden >", order.email, "123-456", ""]
end
end
end

View File

@@ -89,20 +89,39 @@ RSpec.describe "Packing Reports" do
permissions_list: [:add_to_order_cycle])
end
it "shows line items supplied by my producers, with names hidden" do
it "shows line items supplied by my producers, with names and contacts hidden" do
expect(report_contents).to include line_item2.product.name
expect(report_data.first["first_name"]).to eq(
'< Hidden >'
)
row = report_data.first
expect(row["customer_code"]).to eq '< Hidden >'
expect(row["first_name"]).to eq '< Hidden >'
expect(row["last_name"]).to eq '< Hidden >'
expect(row["phone"]).to eq '< Hidden >'
end
context "where the distributor allows suppliers to see customer names" do
before do
distributor.update_columns show_customer_names_to_suppliers: true
end
let(:distributor) {
create(:distributor_enterprise, show_customer_names_to_suppliers: true)
}
it "shows line items supplied by my producers, with names shown" do
expect(report_data.first["first_name"]).to eq(order2.bill_address.firstname)
it "shows line items supplied by my producers, with names and contacts shown" do
row = report_data.first
expect(row["customer_code"]).to eq order2.customer.code
expect(row["first_name"]).to eq order2.bill_address.firstname
expect(row["last_name"]).to eq order2.bill_address.lastname
expect(row["phone"]).to eq '< Hidden >'
end
end
context "where the distributor allows suppliers to see customer contact details" do
let(:distributor) {
create(:distributor_enterprise, show_customer_contacts_to_suppliers: true)
}
it "shows line items supplied by my producers, with names and contacts shown" do
row = report_data.first
expect(row["first_name"]).to eq '< Hidden >'
expect(row["last_name"]).to eq '< Hidden >'
expect(row["phone"]).to eq order2.bill_address.phone
end
end

View File

@@ -7,14 +7,37 @@ RSpec.describe Orders::MaskDataService do
let(:distributor) { create(:enterprise) }
let(:order) { create(:order, distributor:, ship_address: create(:address)) }
context 'when displaying customer names is allowed' do
before { distributor.show_customer_names_to_suppliers = true }
it 'masks personal addresses and email' do
shared_examples "mask customer name" do
it 'masks the full name' do
described_class.new(order).call
expect(order.bill_address.attributes).to include(
'firstname' => "< Hidden >",
'lastname' => ''
)
expect(order.ship_address.attributes).to include(
'firstname' => "< Hidden >",
'lastname' => ''
)
end
end
shared_examples "mask customer contact data" do
it 'masks personal phone and email' do
described_class.new(order).call
expect(order.bill_address.attributes).to include('phone' => '')
expect(order.ship_address.attributes).to include('phone' => '')
expect(order.email).to eq("< Hidden >")
end
end
shared_examples "mask customer address" do
it 'masks personal addresses' do
described_class.new(order).call
expect(order.bill_address.attributes).to include(
'phone' => '',
'address1' => '',
'address2' => '',
'city' => '',
@@ -23,26 +46,30 @@ RSpec.describe Orders::MaskDataService do
)
expect(order.ship_address.attributes).to include(
'phone' => '',
'address1' => '',
'address2' => '',
'city' => '',
'zipcode' => '',
'state_id' => nil
)
expect(order.email).to eq('HIDDEN')
end
end
context 'when displaying customer names is allowed' do
before { distributor.show_customer_names_to_suppliers = true }
include_examples "mask customer contact data"
include_examples "mask customer address"
it 'does not mask the full name' do
described_class.new(order).call
expect(order.bill_address.attributes).not_to include(
firstname: 'HIDDEN',
firstname: "< Hidden >",
lastname: ''
)
expect(order.ship_address.attributes).not_to include(
firstname: 'HIDDEN',
firstname: "< Hidden >",
lastname: ''
)
end
@@ -51,42 +78,33 @@ RSpec.describe Orders::MaskDataService do
context 'when displaying customer names is not allowed' do
before { distributor.show_customer_names_to_suppliers = false }
it 'masks personal addresses and email' do
include_examples "mask customer name"
include_examples "mask customer contact data"
include_examples "mask customer address"
end
context 'when displaying customer contact data is allowed' do
before { distributor.show_customer_contacts_to_suppliers = true }
include_examples "mask customer name"
include_examples "mask customer address"
it 'does not mask the phone or email' do
described_class.new(order).call
expect(order.bill_address.attributes).to include(
'phone' => '',
'address1' => '',
'address2' => '',
'city' => '',
'zipcode' => '',
'state_id' => nil
)
expect(order.bill_address.attributes).not_to include('phone' => '')
expect(order.ship_address.attributes).not_to include('phone' => '')
expect(order.ship_address.attributes).to include(
'phone' => '',
'address1' => '',
'address2' => '',
'city' => '',
'zipcode' => '',
'state_id' => nil
)
expect(order.email).to eq('HIDDEN')
expect(order.email).not_to eq("< Hidden >")
end
end
it 'masks the full name' do
described_class.new(order).call
context 'when displaying customer contact data is not allowed' do
before { distributor.show_customer_contacts_to_suppliers = false }
expect(order.bill_address.attributes).to include(
'firstname' => 'HIDDEN',
'lastname' => ''
)
expect(order.ship_address.attributes).to include(
'firstname' => 'HIDDEN',
'lastname' => ''
)
end
include_examples "mask customer name"
include_examples "mask customer contact data"
include_examples "mask customer address"
end
end
end

View File

@@ -100,6 +100,10 @@ RSpec.describe '
expect(page).not_to have_checked_field "enterprise_require_login_false"
# expect(page).to have_checked_field "enterprise_enable_subscriptions_false"
choose('enterprise[show_customer_contacts_to_suppliers]', option: true)
# See also "setting ordering preferences" tested separately.
scroll_to(:bottom)
accept_alert do
scroll_to(:bottom)
@@ -216,6 +220,7 @@ RSpec.describe '
"enterprise_preferred_product_selection_from_inventory_only_false"
)
# Save changes
click_button 'Update'
expect(flash_message).to eq('Enterprise "Eaterprises" has been successfully updated!')
@@ -246,6 +251,7 @@ RSpec.describe '
)
expect(page).to have_checked_field "enterprise_require_login_true"
expect(page).to have_checked_field "enterprise_enable_subscriptions_true"
expect(page).to have_checked_field 'enterprise[show_customer_contacts_to_suppliers]', with: true
# Back navigation loads the tab content
page.execute_script('window.history.back()')