Merge pull request #8598 from seballot/oc-notify-producers

Configuration to automatically notify producers on order cycle close
This commit is contained in:
Filipe
2021-12-23 21:31:45 +00:00
committed by GitHub
17 changed files with 218 additions and 72 deletions

View File

@@ -4,18 +4,15 @@
background-color: $spree-light-blue;
border: 1px solid $pale-blue;
margin-bottom: 20px;
padding: 20px 20px 0px 20px;
}
.row{
margin: 0px -4px;
padding: 20px 0px;
.column.alpha, .columns.alpha {
padding-left: 20px;
}
.column.omega, .columns.omega {
padding-right: 20px;
}
#toggle_settings {
display: flex;
align-items: center;
i {
display: inline-flex;
margin-left: 5px;
}
}

View File

@@ -0,0 +1,63 @@
input[type="submit"], input[type="button"], button, .button {
position: relative;
cursor: pointer;
font-size: 85%;
@include border-radius($border-radius);
display: inline-block;
padding: 8px 15px;
border: none;
background-color: $color-btn-bg;
color: $color-btn-text;
text-transform: uppercase;
font-weight: 600 !important;
&:before {
font-weight: normal !important;
}
&:visited, &:active, &:focus { color: $color-btn-text }
&:hover {
background-color: $color-btn-hover-bg;
color: $color-btn-hover-text;
}
&:active:focus {
box-shadow: 0 0 8px 0 darken($color-btn-hover-bg, 5) inset;
}
&.fullwidth {
width: 100%;
text-align: center;
}
&.secondary {
background-color: transparent;
border: 1px solid $color-btn-bg;
color: $color-btn-bg;
&:hover, &:active, &:focus {
background-color: #ebf3fb;
}
&:active:focus {
box-shadow: none;
}
}
.badge {
position: absolute;
top: 0;
right: 0;
transform: translateY(-50%);
font-size: 10px;
text-transform: capitalize;
padding: 0px 5px;
border-radius: 3px;
&:before { padding: 0 }
&.danger { background-color: $warning-red; }
&.success { background-color: $spree-green; }
}
}

View File

@@ -1,10 +1,5 @@
@import "variables";
input[type="submit"], input[type="button"], button, .button {
cursor: pointer;
font-size: 85%;
}
.text-center {
text-align: center;
}

View File

@@ -54,53 +54,6 @@ label {
.label-block label { display: block }
input[type="submit"],
input[type="button"],
button, .button {
@include border-radius($border-radius);
display: inline-block;
padding: 8px 15px;
border: none;
background-color: $color-btn-bg;
color: $color-btn-text;
text-transform: uppercase;
font-weight: 600 !important;
&:before {
font-weight: normal !important;
}
&:visited, &:active, &:focus { color: $color-btn-text }
&:hover {
background-color: $color-btn-hover-bg;
color: $color-btn-hover-text;
}
&:active:focus {
box-shadow: 0 0 8px 0 darken($color-btn-hover-bg, 5) inset;
}
&.fullwidth {
width: 100%;
text-align: center;
}
&.secondary {
background-color: transparent;
border: 1px solid $color-btn-bg;
color: $color-btn-bg;
&:hover, &:active, &:focus {
background-color: #ebf3fb;
}
&:active:focus {
box-shadow: none;
}
}
}
span.info {
font-style: italic;
font-size: 85%;

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
class OrderCycleClosingJob < ActiveJob::Base
def perform
return if recently_closed_order_cycles.empty?
send_notifications
mark_as_processed
end
private
def recently_closed_order_cycles
@recently_closed_order_cycles ||= OrderCycle.closed.unprocessed.
where(
'order_cycles.orders_close_at BETWEEN (?) AND (?)', 1.hour.ago, Time.zone.now
).select(:id, :automatic_notifications).to_a
end
def send_notifications
recently_closed_order_cycles.each do |oc|
OrderCycleNotificationJob.perform_later(oc.id) if oc.automatic_notifications?
end
end
def mark_as_processed
OrderCycle.where(id: recently_closed_order_cycles).update_all(
processed_at: Time.zone.now,
updated_at: Time.zone.now
)
end
end

View File

@@ -28,6 +28,8 @@ class OrderCycle < ApplicationRecord
attr_accessor :incoming_exchanges, :outgoing_exchanges
before_update :reset_processed_at, if: :will_save_change_to_orders_close_at?
validates :name, :coordinator_id, presence: true
validate :orders_close_at_after_orders_open_at?
@@ -52,6 +54,7 @@ class OrderCycle < ApplicationRecord
where('order_cycles.orders_close_at < ?',
Time.zone.now).order("order_cycles.orders_close_at DESC")
}
scope :unprocessed, -> { where(processed_at: nil) }
scope :undated, -> { where('order_cycles.orders_open_at IS NULL OR orders_close_at IS NULL') }
scope :dated, -> { where('orders_open_at IS NOT NULL AND orders_close_at IS NOT NULL') }
@@ -275,4 +278,10 @@ class OrderCycle < ApplicationRecord
errors.add(:orders_close_at, :after_orders_open_at)
end
def reset_processed_at
return unless orders_close_at.present? && orders_close_at_was.present?
self.processed_at = nil if orders_close_at > orders_close_at_was
end
end

View File

@@ -16,6 +16,7 @@ module PermittedAttributes
[
:name, :orders_open_at, :orders_close_at, :coordinator_id,
:preferred_product_selection_from_coordinator_inventory_only,
:automatic_notifications,
{ schedule_ids: [], coordinator_fee_ids: [] }
]
end

View File

@@ -1,20 +1,27 @@
.row
.alpha.omega.sixteen.columns
%h3= t('.title')
%h3= t('.title')
= form_for [main_app, :admin, @order_cycle] do |f|
.row
.six.columns.alpha
.three.columns.alpha
= f.label "enterprise_preferred_product_selection_from_coordinator_inventory_only", t('admin.order_cycles.edit.choose_products_from')
.with-tip{'data-powertip' => t('.choose_product_tip', inventory: @order_cycle.coordinator.name)}
.with-tip{ 'data-powertip' => t('.choose_product_tip', inventory: @order_cycle.coordinator.name) }
%a= t('admin.whats_this')
.four.columns
= f.radio_button :preferred_product_selection_from_coordinator_inventory_only, true
= f.label :preferred_product_selection_from_coordinator_inventory_only, t('.preferred_product_selection_from_coordinator_inventory_only_here')
.six.columns.omega
.four.columns.omega
= f.radio_button :preferred_product_selection_from_coordinator_inventory_only, false
= f.label :preferred_product_selection_from_coordinator_inventory_only, t('.preferred_product_selection_from_coordinator_inventory_only_all')
.row
.alpha.three.columns
= f.label :automatic_notifications, t('.automatic_notifications')
.with-tip{ 'data-powertip' => t('.automatic_notifications_tip') }
%a= t('admin.whats_this')
.omega.eight.columns
= f.check_box :automatic_notifications
.row
.sixteen.columns.alpha.omega.text-center
%input{ type: 'submit', value: t('.save_reload') }

View File

@@ -2,7 +2,13 @@
- content_for :page_actions do
- if can? :notify_producers, @order_cycle
%li
= button_to t(:notify_producers), main_app.notify_producers_admin_order_cycle_path, :id => 'admin_notify_producers', :confirm => t(:are_you_sure)
- processed = @order_cycle.processed_at.present?
- url = main_app.notify_producers_admin_order_cycle_path
- confirm_msg = "#{t('.notify_producers_tip')} #{t(:are_you_sure)}"
%a.button.icon-email.with-tip{ href: url, data: { method: 'post', confirm: confirm_msg }, 'data-powertip': t('.notify_producers_tip') }
= processed ? t('.re_notify_producers') : t(:notify_producers)
- if processed
.badge.icon-ok.success
- content_for :page_title do
= t :edit_order_cycle

View File

@@ -1038,6 +1038,8 @@ en:
back_to_list: "Back To List"
save_and_back_to_list: "Save and Back to List"
choose_products_from: "Choose Products From:"
re_notify_producers: Re notify producers
notify_producers_tip: This will send an email to each producer with the list of their orders.
incoming:
incoming: "Incoming"
supplier: "Supplier"
@@ -1078,6 +1080,8 @@ en:
add_supplier: 'Add supplier'
add_distributor: 'Add distributor'
advanced_settings:
automatic_notifications: Automatic notifications
automatic_notifications_tip: Automatically notify producers with their orders via emails when order cycles close
title: Advanced Settings
choose_product_tip: You can restrict products incoming and outgoing to only %{inventory}'s inventory.
preferred_product_selection_from_coordinator_inventory_only_here: Coordinator's Inventory Only
@@ -2403,7 +2407,7 @@ See the %{link} to find out more about %{sitename}'s features and to start using
new_order_cycle: "New Order Cycle"
new_order_cycle_tooltip: "Open shop for a certain time period"
select_a_coordinator_for_your_order_cycle: "Select a coordinator for your order cycle"
notify_producers: 'Notify producers'
notify_producers: 'Notify producers'
edit_order_cycle: "Edit Order Cycle"
roles: "Roles"
update: "Update"

View File

@@ -14,3 +14,5 @@
every: "5m"
SubscriptionConfirmJob:
every: "5m"
OrderCycleClosingJob:
every: "5m"

View File

@@ -0,0 +1,5 @@
class AddProcessedAtToOrderCycles < ActiveRecord::Migration[6.1]
def change
add_column :order_cycles, :processed_at, :datetime
end
end

View File

@@ -0,0 +1,5 @@
class AddAutomaticNotificationsToOrderCycles < ActiveRecord::Migration[6.1]
def change
add_column :order_cycles, :automatic_notifications, :boolean, default: false
end
end

View File

@@ -293,6 +293,8 @@ ActiveRecord::Schema.define(version: 2021_12_17_094141) do
t.integer "coordinator_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.datetime "processed_at"
t.boolean "automatic_notifications", default: false
end
create_table "producer_properties", force: :cascade do |t|

View File

@@ -216,6 +216,17 @@ module Admin
spree_put :update, params.
merge(order_cycle: { preferred_product_selection_from_coordinator_inventory_only: true })
end
it "can update preference automatic_notifications" do
expect(OrderCycleForm).to receive(:new).
with(order_cycle,
{ "automatic_notifications" => true },
anything) { form_mock }
allow(form_mock).to receive(:save) { true }
spree_put :update, params.
merge(order_cycle: { automatic_notifications: true })
end
end
end

View File

@@ -0,0 +1,27 @@
# frozen_string_literal: true
require 'spec_helper'
describe OrderCycleClosingJob do
let(:order_cycle1) {
create(:order_cycle, automatic_notifications: true, orders_close_at: Time.zone.now - 1.minute)
}
let(:order_cycle2) {
create(:order_cycle, automatic_notifications: true, orders_close_at: Time.zone.now + 1.minute)
}
let(:order_cycle3) {
create(:order_cycle, automatic_notifications: false, orders_close_at: Time.zone.now - 1.minute)
}
it "sends notifications for recently closed order cycles with automatic notifications enabled" do
expect(OrderCycleNotificationJob).to receive(:perform_later).with(order_cycle1.id)
expect(OrderCycleNotificationJob).to_not receive(:perform_later).with(order_cycle2.id)
expect(OrderCycleNotificationJob).to_not receive(:perform_later).with(order_cycle3.id)
OrderCycleClosingJob.perform_now
end
it "marks order cycles as processed" do
expect{ OrderCycleClosingJob.perform_now }.to change{ order_cycle1.reload.processed_at }
end
end

View File

@@ -371,7 +371,9 @@ describe OrderCycle do
it "clones itself" do
coordinator = create(:enterprise);
oc = create(:simple_order_cycle,
coordinator_fees: [create(:enterprise_fee, enterprise: coordinator)], preferred_product_selection_from_coordinator_inventory_only: true)
coordinator_fees: [create(:enterprise_fee, enterprise: coordinator)],
preferred_product_selection_from_coordinator_inventory_only: true,
automatic_notifications: true)
ex1 = create(:exchange, order_cycle: oc)
ex2 = create(:exchange, order_cycle: oc)
oc.clone!
@@ -382,6 +384,7 @@ describe OrderCycle do
expect(occ.orders_close_at).to be_nil
expect(occ.coordinator).not_to be_nil
expect(occ.preferred_product_selection_from_coordinator_inventory_only).to be true
expect(occ.automatic_notifications).to eq(oc.automatic_notifications)
expect(occ.coordinator).to eq(oc.coordinator)
expect(occ.coordinator_fee_ids).not_to be_empty
@@ -542,6 +545,30 @@ describe OrderCycle do
end
end
describe "processed_at " do
let!(:oc) {
create(:simple_order_cycle, orders_open_at: 1.week.ago, orders_close_at: 1.day.ago, processed_at: 1.hour.ago)
}
it "reset processed_at if close date change in future" do
expect(oc.processed_at).to_not be_nil
oc.update!(orders_close_at: 1.week.from_now)
expect(oc.processed_at).to be_nil
end
it "it does not reset processed_at if close date change in the past" do
expect(oc.processed_at).to_not be_nil
oc.update!(orders_close_at: 2.days.ago)
expect(oc.processed_at).to_not be_nil
end
it "it does not reset processed_at if close date do not change" do
expect(oc.processed_at).to_not be_nil
oc.update!(orders_open_at: 2.weeks.ago)
expect(oc.processed_at).to_not be_nil
end
end
def core_exchange_attributes(exchange)
exterior_attribute_keys = %w(id order_cycle_id created_at updated_at)
exchange.attributes.