spree_cm_commissioner 1.9.2 → 1.10.0.pre.pre
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/test_and_build_gem.yml +86 -0
- data/.gitignore +2 -1
- data/Gemfile.lock +22 -1
- data/app/controllers/spree/admin/events_controller.rb +47 -0
- data/app/controllers/spree/admin/prototypes_controller_decorator.rb +20 -0
- data/app/controllers/spree/admin/stock_managements_controller.rb +17 -1
- data/app/controllers/spree/api/v2/storefront/accommodations/variants_controller.rb +42 -0
- data/app/controllers/spree/api/v2/storefront/accommodations_controller.rb +14 -31
- data/app/finders/spree_cm_commissioner/accommodations/find.rb +40 -0
- data/app/finders/spree_cm_commissioner/accommodations/find_variant.rb +35 -0
- data/app/interactors/spree_cm_commissioner/create_event.rb +65 -0
- data/app/interactors/spree_cm_commissioner/firebase_email_fetcher.rb +30 -4
- data/app/interactors/spree_cm_commissioner/firebase_email_fetcher_cron_executor.rb +23 -0
- data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +25 -0
- data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +71 -0
- data/app/interactors/spree_cm_commissioner/vattanac_bank_initiator.rb +25 -6
- data/app/jobs/spree_cm_commissioner/firebase_email_fetcher_job.rb +9 -0
- data/app/jobs/spree_cm_commissioner/inventory_item_syncer_job.rb +7 -0
- data/app/jobs/spree_cm_commissioner/stock/permanent_inventory_items_generator_job.rb +9 -0
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +26 -0
- data/app/models/concerns/spree_cm_commissioner/product_type.rb +10 -0
- data/app/models/spree_cm_commissioner/inventory.rb +11 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +37 -0
- data/app/models/spree_cm_commissioner/invite_user_taxon.rb +1 -0
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +15 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +10 -0
- data/app/models/spree_cm_commissioner/prototype_decorator.rb +9 -0
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +40 -0
- data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +114 -0
- data/app/models/spree_cm_commissioner/redis_stock/line_items_cached_inventory_items_builder.rb +42 -0
- data/app/models/spree_cm_commissioner/redis_stock/variant_cached_inventory_items_builder.rb +27 -0
- data/app/models/spree_cm_commissioner/stock/availability_checker.rb +27 -25
- data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +2 -1
- data/app/models/spree_cm_commissioner/stock/line_item_availability_checker.rb +3 -3
- data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +44 -0
- data/app/models/spree_cm_commissioner/stock_movement_decorator.rb +34 -0
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +2 -1
- data/app/models/spree_cm_commissioner/user_decorator.rb +5 -1
- data/app/models/spree_cm_commissioner/variant_decorator.rb +24 -17
- data/app/overrides/spree/admin/prototypes/_form/description.html.erb.deface +6 -0
- data/app/overrides/spree/admin/prototypes/_form/icon.html.erb.deface +8 -0
- data/app/overrides/spree/admin/prototypes/_form/product_type.html.erb.deface +2 -2
- data/app/overrides/spree/admin/shared/sub_menu/_product/events_tab.html.erb.deface +3 -0
- data/app/request_schemas/spree_cm_commissioner/accommodation_request_schema.rb +3 -0
- data/app/request_schemas/spree_cm_commissioner/application_request_schema.rb +1 -1
- data/app/request_schemas/spree_cm_commissioner/variant_request_schema.rb +19 -0
- data/app/serializers/spree/v2/storefront/accommodation_serializer.rb +2 -0
- data/app/serializers/spree/v2/tenant/payment_method_serializer.rb +12 -0
- data/app/serializers/spree/v2/tenant/user_serializer.rb +1 -1
- data/app/views/spree/admin/events/_search_form.html.erb +61 -0
- data/app/views/spree/admin/events/_tab.html.erb +25 -0
- data/app/views/spree/admin/events/_table.html.erb +27 -0
- data/app/views/spree/admin/events/index.html.erb +15 -0
- data/app/views/spree/admin/stock_managements/_events_popover.html.erb +17 -0
- data/app/views/spree/admin/stock_managements/calendar.html.erb +32 -0
- data/app/views/spree/admin/stock_managements/index.html.erb +31 -5
- data/app/views/spree_cm_commissioner/crew_invite_mailer/_mailer_stylesheets.html.erb +2 -2
- data/app/views/spree_cm_commissioner/crew_invite_mailer/send_crew_invite_email.html.erb +1 -1
- data/config/initializers/user_manager_decorator.rb +26 -0
- data/config/routes.rb +12 -2
- data/db/migrate/20250304293518_create_cm_inventory_items.rb +21 -0
- data/db/migrate/20250425084929_add_description_to_spree_prototypes.rb +5 -0
- data/db/migrate/20250425085938_add_preferences_to_spree_prototypes.rb +5 -0
- data/db/migrate/20250428025645_add_slug_to_spree_prototypes.rb +6 -0
- data/db/migrate/20250429094228_add_lock_version_to_cm_inventory_items.rb +5 -0
- data/docker-compose.yml +1 -1
- data/lib/generators/spree_cm_commissioner/install/install_generator.rb +2 -2
- data/lib/generators/spree_cm_commissioner/install/templates/app/javascript/{spree_cm_commissioner → spree_dashboard/spree_cm_commissioner}/utilities.js +4 -0
- data/lib/spree_cm_commissioner/cached_inventory_item.rb +23 -0
- data/lib/spree_cm_commissioner/calendar_event.rb +11 -1
- data/lib/spree_cm_commissioner/test_helper/factories/inventory_item_factory.rb +9 -0
- data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +28 -6
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +34 -0
- data/lib/tasks/create_default_non_permanent_inventory_items.rake +16 -0
- data/lib/tasks/fetch_email.rake +7 -0
- data/lib/tasks/generate_inventory_items.rake +7 -0
- data/spree_cm_commissioner.gemspec +5 -0
- metadata +88 -7
- data/app/queries/spree_cm_commissioner/variant_availability/non_permanent_stock_query.rb +0 -45
- data/app/queries/spree_cm_commissioner/variant_availability/permanent_stock_query.rb +0 -55
@@ -0,0 +1,71 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
module Stock
|
3
|
+
class PermanentInventoryItemsGenerator < BaseInteractor
|
4
|
+
delegate :variant_ids, to: :context
|
5
|
+
|
6
|
+
def variants_per_batch = 1000
|
7
|
+
|
8
|
+
def call
|
9
|
+
variants.in_batches(of: variants_per_batch) do |batch|
|
10
|
+
generate_inventory_items_for_batch(batch)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def generate_inventory_items_for_batch(batch)
|
17
|
+
total_on_hand_by_variant = total_on_hand_for(batch)
|
18
|
+
batch.each do |variant|
|
19
|
+
count_on_hand = total_on_hand_by_variant[variant.id] || 0
|
20
|
+
generate_inventory_items_for_variant(variant, count_on_hand)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate_inventory_items_for_variant(variant, count_on_hand)
|
25
|
+
inventory_dates_for(variant).each do |inventory_date|
|
26
|
+
next if inventory_exist?(variant, inventory_date)
|
27
|
+
|
28
|
+
create_inventory_item(variant, inventory_date, count_on_hand)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def inventory_dates_for(variant)
|
33
|
+
start_date = Time.zone.tomorrow
|
34
|
+
end_date = Time.zone.today + variant.pre_inventory_days
|
35
|
+
|
36
|
+
(start_date..end_date)
|
37
|
+
end
|
38
|
+
|
39
|
+
def inventory_exist?(variant, inventory_date)
|
40
|
+
variant.inventory_items.exists?(inventory_date: inventory_date)
|
41
|
+
end
|
42
|
+
|
43
|
+
def create_inventory_item(variant, inventory_date, count_on_hand)
|
44
|
+
variant.inventory_items.create!(
|
45
|
+
inventory_date: inventory_date,
|
46
|
+
quantity_available: count_on_hand,
|
47
|
+
max_capacity: count_on_hand,
|
48
|
+
product_type: variant.product_type
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
# Returns a hash: { variant_id => total_on_hand, ... }
|
53
|
+
def total_on_hand_for(variants)
|
54
|
+
variant_ids = variants.pluck(:id)
|
55
|
+
|
56
|
+
Spree::StockItem
|
57
|
+
.joins(:stock_location)
|
58
|
+
.where(deleted_at: nil, variant_id: variant_ids)
|
59
|
+
.where(spree_stock_locations: { active: true })
|
60
|
+
.group(:variant_id)
|
61
|
+
.sum(:count_on_hand)
|
62
|
+
end
|
63
|
+
|
64
|
+
def variants
|
65
|
+
scope = Spree::Variant.active.with_permanent_stock.where(is_master: false).includes(:product)
|
66
|
+
scope = scope.where(id: variant_ids) if variant_ids.present?
|
67
|
+
scope
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -25,10 +25,8 @@ module SpreeCmCommissioner
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def verify_signature
|
28
|
-
public_key = ENV['VATTANAC_PUBLIC_KEY'].presence || Rails.application.credentials.vattanac.public_key
|
29
|
-
|
30
28
|
rsa_service = SpreeCmCommissioner::RsaService.new(
|
31
|
-
public_key:
|
29
|
+
public_key: vattanac_public_key
|
32
30
|
)
|
33
31
|
|
34
32
|
return if rsa_service.verify(context.encrypted_data, context.signature)
|
@@ -37,8 +35,6 @@ module SpreeCmCommissioner
|
|
37
35
|
end
|
38
36
|
|
39
37
|
def decrypt_payload
|
40
|
-
aes_key = ENV['VATTANAC_AES_SECRET_KEY'].presence || Rails.application.credentials.vattanac.aes_secret_key
|
41
|
-
|
42
38
|
context.fail!(message: 'Invalid AES key length', status: :unprocessable_entity) unless aes_key
|
43
39
|
|
44
40
|
begin
|
@@ -99,18 +95,41 @@ module SpreeCmCommissioner
|
|
99
95
|
|
100
96
|
def construct_data
|
101
97
|
user = context.user
|
102
|
-
|
98
|
+
|
99
|
+
raw_data = {
|
103
100
|
sessionId: session_id,
|
104
101
|
name: user.full_name,
|
105
102
|
phone: user.phone_number,
|
106
103
|
email: user.email,
|
107
104
|
webUrl: "#{Spree::Store.default.formatted_url}/vattanac_bank_web_app?session_id=#{session_id}"
|
108
105
|
}
|
106
|
+
|
107
|
+
json_data = raw_data.to_json
|
108
|
+
|
109
|
+
encrypted_data = SpreeCmCommissioner::AesEncryptionService.encrypt(json_data, aes_key)
|
110
|
+
|
111
|
+
rsa_service = SpreeCmCommissioner::RsaService.new(private_key: bookmeplus_private_key)
|
112
|
+
|
113
|
+
signed_data = rsa_service.sign(encrypted_data)
|
114
|
+
|
115
|
+
context.data = signed_data
|
109
116
|
end
|
110
117
|
|
111
118
|
def session_id
|
112
119
|
payload = { user_id: context.user.id }
|
113
120
|
SpreeCmCommissioner::UserSessionJwtToken.encode(payload, context.user.reload.secure_token)
|
114
121
|
end
|
122
|
+
|
123
|
+
def aes_key
|
124
|
+
ENV['VATTANAC_AES_SECRET_KEY'].presence || Rails.application.credentials.vattanac.aes_secret_key
|
125
|
+
end
|
126
|
+
|
127
|
+
def bookmeplus_private_key
|
128
|
+
ENV['BOOKMEPLUS_PRIVATE_KEY'].presence || Rails.application.credentials.bookmeplus.private_key
|
129
|
+
end
|
130
|
+
|
131
|
+
def vattanac_public_key
|
132
|
+
ENV['VATTANAC_PUBLIC_KEY'].presence || Rails.application.credentials.vattanac.public_key
|
133
|
+
end
|
115
134
|
end
|
116
135
|
end
|
@@ -13,10 +13,13 @@ module SpreeCmCommissioner
|
|
13
13
|
state_machine.after_transition to: :complete, do: :notify_order_complete_telegram_notification_to_user, unless: :subscription?
|
14
14
|
state_machine.after_transition to: :complete, do: :send_order_complete_telegram_alert_to_vendors, unless: :need_confirmation?
|
15
15
|
state_machine.after_transition to: :complete, do: :send_order_complete_telegram_alert_to_store, unless: :need_confirmation?
|
16
|
+
state_machine.around_transition to: :complete, do: :handle_unstock_in_redis
|
16
17
|
|
17
18
|
state_machine.after_transition to: :resumed, do: :precalculate_conversion
|
19
|
+
state_machine.around_transition to: :resumed, do: :handle_unstock_in_redis
|
18
20
|
|
19
21
|
state_machine.after_transition to: :canceled, do: :precalculate_conversion
|
22
|
+
state_machine.after_transition to: :canceled, do: :restock_inventory_in_redis!
|
20
23
|
|
21
24
|
scope :accepted, -> { where(request_state: 'accepted') }
|
22
25
|
|
@@ -66,6 +69,29 @@ module SpreeCmCommissioner
|
|
66
69
|
end
|
67
70
|
end
|
68
71
|
|
72
|
+
def handle_unstock_in_redis
|
73
|
+
ActiveRecord::Base.transaction do
|
74
|
+
yield # Equal to block.call
|
75
|
+
|
76
|
+
# After the transition is complete, the following code will execute first before proceeding to other `after_transition` callbacks.
|
77
|
+
# This ensures that if `unstock_inventory_in_redis!` fails, the state will be rolled back,
|
78
|
+
# and neither the `finalize!` method nor any notifications will be triggered.
|
79
|
+
# The payment will be reversed in vPago gem, and `Spree::Checkout::Complete` will be called, which checks `order.reload.complete?`.
|
80
|
+
# This is critical because if the order state is complete, the payment will be marked as paid.
|
81
|
+
CmAppLogger.log(label: 'order_state_machine_before_unstock', data: { order_id: id, state: state })
|
82
|
+
unstock_inventory_in_redis!
|
83
|
+
# We rollback only order state, and we keep payment state as it is.
|
84
|
+
# We implement payment in vPago gem, and it will be reversed in the gem.
|
85
|
+
# Some bank has api for refund, but some don't have the api to refund yet. So we keep the payment state as it is and refund manually.
|
86
|
+
CmAppLogger.log(label: 'order_state_machine_after_unstock', data: { order_id: id, state: state })
|
87
|
+
end
|
88
|
+
rescue StandardError => e
|
89
|
+
CmAppLogger.log(label: 'order_state_machine',
|
90
|
+
data: { order_id: id, error: e.message, type: e.class.name, backtrace: e.backtrace.first(5).join("\n") }
|
91
|
+
)
|
92
|
+
raise e
|
93
|
+
end
|
94
|
+
|
69
95
|
def generate_bib_number
|
70
96
|
line_items.find_each(&:generate_remaining_guests)
|
71
97
|
|
@@ -6,10 +6,20 @@ module SpreeCmCommissioner
|
|
6
6
|
extend ActiveSupport::Concern
|
7
7
|
|
8
8
|
PRODUCT_TYPES = %i[accommodation service ecommerce transit].freeze
|
9
|
+
PERMANENT_STOCK_PRODUCT_TYPES = %w[accommodation service transit].freeze
|
10
|
+
PRE_INVENTORY_DAYS = { 'transit' => 90, 'accommodation' => 365, 'service' => 30 }.freeze
|
9
11
|
|
10
12
|
included do
|
11
13
|
enum product_type: PRODUCT_TYPES if table_exists? && column_names.include?('product_type')
|
12
14
|
enum primary_product_type: PRODUCT_TYPES if table_exists? && column_names.include?('primary_product_type')
|
13
15
|
end
|
16
|
+
|
17
|
+
def permanent_stock?
|
18
|
+
PERMANENT_STOCK_PRODUCT_TYPES.include?(product_type)
|
19
|
+
end
|
20
|
+
|
21
|
+
def pre_inventory_days
|
22
|
+
PRE_INVENTORY_DAYS[product_type]
|
23
|
+
end
|
14
24
|
end
|
15
25
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
class Inventory
|
3
|
+
include ActiveModel::Model
|
4
|
+
|
5
|
+
attr_accessor :variant_id, :inventory_date, :quantity_available, :max_capacity, :product_type
|
6
|
+
|
7
|
+
validates :variant_id, presence: true
|
8
|
+
validates :quantity_available, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
|
9
|
+
validates :max_capacity, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
class InventoryItem < ApplicationRecord
|
3
|
+
include SpreeCmCommissioner::ProductType
|
4
|
+
|
5
|
+
# Association
|
6
|
+
belongs_to :variant, class_name: 'Spree::Variant'
|
7
|
+
|
8
|
+
# Validation
|
9
|
+
validates :quantity_available, numericality: { greater_than_or_equal_to: 0 }
|
10
|
+
validates :max_capacity, numericality: { greater_than_or_equal_to: 0 } # Originally inventory of each variant.
|
11
|
+
validates :inventory_date, presence: true, if: -> { permanent_stock? }
|
12
|
+
validates :variant_id, uniqueness: { scope: :inventory_date, message: -> (object, _data) { "The variant is taken on #{object.inventory_date}" } }
|
13
|
+
|
14
|
+
# Scope
|
15
|
+
scope :for_product, -> (type) { where(product_type: type) }
|
16
|
+
scope :active, -> { where(inventory_date: nil).or(where('inventory_date >= ?', Time.zone.today)) }
|
17
|
+
|
18
|
+
def adjust_quantity!(quantity)
|
19
|
+
with_lock do
|
20
|
+
self.max_capacity = max_capacity + quantity
|
21
|
+
self.quantity_available = quantity_available + quantity
|
22
|
+
|
23
|
+
save!
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def active?
|
28
|
+
inventory_date.nil? || inventory_date >= Time.zone.today
|
29
|
+
end
|
30
|
+
|
31
|
+
def redis_expired_in
|
32
|
+
expired_in = 31_536_000 # 1 year for normal stock
|
33
|
+
expired_in = Time.parse(inventory_date.to_s).end_of_day.to_i - Time.zone.now.to_i if inventory_date.present?
|
34
|
+
[expired_in, 0].max
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -2,6 +2,7 @@ module SpreeCmCommissioner
|
|
2
2
|
class InviteUserTaxon < SpreeCmCommissioner::Base
|
3
3
|
belongs_to :user_taxon, class_name: 'SpreeCmCommissioner::UserTaxon'
|
4
4
|
belongs_to :invite, class_name: 'SpreeCmCommissioner::Invite'
|
5
|
+
has_one :inviter, class_name: 'Spree::User', through: :invite
|
5
6
|
after_create :send_crew_invite_email
|
6
7
|
|
7
8
|
def send_crew_invite_email
|
@@ -9,6 +9,7 @@ module SpreeCmCommissioner
|
|
9
9
|
base.has_one :google_wallet, class_name: 'SpreeCmCommissioner::GoogleWallet', through: :product
|
10
10
|
|
11
11
|
base.has_many :option_types, through: :product
|
12
|
+
base.has_many :inventory_items, through: :variant
|
12
13
|
base.has_many :taxons, class_name: 'Spree::Taxon', through: :product
|
13
14
|
base.has_many :guests, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
|
14
15
|
base.has_many :pending_guests, pending_guests_query, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
|
@@ -58,6 +58,13 @@ module SpreeCmCommissioner
|
|
58
58
|
end
|
59
59
|
end
|
60
60
|
|
61
|
+
# override
|
62
|
+
# spree use this method to check stock availability & consider whether :order can continue to next state.
|
63
|
+
def insufficient_stock_lines
|
64
|
+
checker = SpreeCmCommissioner::Stock::OrderAvailabilityChecker.new(self)
|
65
|
+
checker.insufficient_stock_lines
|
66
|
+
end
|
67
|
+
|
61
68
|
def ticket_seller_user?
|
62
69
|
return false if user.nil?
|
63
70
|
|
@@ -194,6 +201,14 @@ module SpreeCmCommissioner
|
|
194
201
|
|
195
202
|
private
|
196
203
|
|
204
|
+
def unstock_inventory_in_redis!
|
205
|
+
SpreeCmCommissioner::RedisStock::InventoryUpdater.new(line_item_ids).unstock!
|
206
|
+
end
|
207
|
+
|
208
|
+
def restock_inventory_in_redis!
|
209
|
+
SpreeCmCommissioner::RedisStock::InventoryUpdater.new(line_item_ids).restock!
|
210
|
+
end
|
211
|
+
|
197
212
|
# override :spree_api
|
198
213
|
def webhook_payload_body
|
199
214
|
resource_serializer.new(
|
@@ -33,6 +33,7 @@ module SpreeCmCommissioner
|
|
33
33
|
base.has_one :google_wallet, class_name: 'SpreeCmCommissioner::GoogleWallet', dependent: :destroy
|
34
34
|
|
35
35
|
base.has_many :complete_line_items, through: :classifications, source: :line_items
|
36
|
+
base.has_many :inventory_items, through: :variants
|
36
37
|
|
37
38
|
base.has_many :product_places, class_name: 'SpreeCmCommissioner::ProductPlace', dependent: :destroy
|
38
39
|
base.has_many :places, through: :product_places
|
@@ -57,6 +58,9 @@ module SpreeCmCommissioner
|
|
57
58
|
base.scope :subscribable, -> { where(subscribable: 1) }
|
58
59
|
|
59
60
|
base.validate :validate_event_taxons, if: -> { taxons.event.present? }
|
61
|
+
|
62
|
+
base.validate :validate_product_date, if: -> { available_on.present? && discontinue_on.present? }
|
63
|
+
|
60
64
|
base.validates :commission_rate, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true
|
61
65
|
|
62
66
|
base.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status vendor_id short_name route_type]
|
@@ -93,6 +97,12 @@ module SpreeCmCommissioner
|
|
93
97
|
errors.add(:taxons, 'Event Taxon can\'t not be more than 1') if taxons.event.size > 1
|
94
98
|
errors.add(:taxons, 'Must add event date to taxon') if taxons.event.first.from_date.nil? || taxons.event.first.to_date.nil?
|
95
99
|
end
|
100
|
+
|
101
|
+
def validate_product_date
|
102
|
+
return unless discontinue_on < available_on
|
103
|
+
|
104
|
+
errors.add(:discontinue_on, 'must be after the available on date')
|
105
|
+
end
|
96
106
|
end
|
97
107
|
end
|
98
108
|
|
@@ -1,6 +1,9 @@
|
|
1
1
|
module SpreeCmCommissioner
|
2
2
|
module PrototypeDecorator
|
3
3
|
def self.prepended(base)
|
4
|
+
base.extend FriendlyId
|
5
|
+
base.friendly_id :name, use: :slugged
|
6
|
+
|
4
7
|
base.include SpreeCmCommissioner::ProductType
|
5
8
|
|
6
9
|
base.has_many :variant_kind_option_types, -> { where(kind: :variant).order(:position) },
|
@@ -10,6 +13,12 @@ module SpreeCmCommissioner
|
|
10
13
|
through: :option_type_prototypes, source: :option_type
|
11
14
|
|
12
15
|
base.has_many :option_values, through: :option_types
|
16
|
+
|
17
|
+
base.preference :icon, :string
|
18
|
+
|
19
|
+
def icon
|
20
|
+
preferred_icon
|
21
|
+
end
|
13
22
|
end
|
14
23
|
end
|
15
24
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
module RedisStock
|
3
|
+
class CachedInventoryItemsBuilder
|
4
|
+
attr_reader :inventory_items
|
5
|
+
|
6
|
+
def initialize(inventory_items)
|
7
|
+
@inventory_items = inventory_items
|
8
|
+
end
|
9
|
+
|
10
|
+
# output: [ CachedInventoryItem(...), CachedInventoryItem(...) ]
|
11
|
+
def call
|
12
|
+
keys = inventory_items.map { |item| "inventory:#{item.id}" }
|
13
|
+
return [] unless keys.any?
|
14
|
+
|
15
|
+
counts = SpreeCmCommissioner.redis_pool.with { |redis| redis.mget(*keys) }
|
16
|
+
inventory_items.map.with_index do |inventory_item, i|
|
17
|
+
::SpreeCmCommissioner::CachedInventoryItem.new(
|
18
|
+
inventory_key: keys[i],
|
19
|
+
active: inventory_item.active?,
|
20
|
+
quantity_available: cache_inventory(keys[i], inventory_item, counts[i]),
|
21
|
+
inventory_item_id: inventory_item.id,
|
22
|
+
variant_id: inventory_item.variant_id
|
23
|
+
)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def cache_inventory(key, inventory_item, count_in_redis)
|
30
|
+
return count_in_redis.to_i if count_in_redis.present?
|
31
|
+
|
32
|
+
SpreeCmCommissioner.redis_pool.with do |redis|
|
33
|
+
redis.set(key, inventory_item.quantity_available, ex: inventory_item.redis_expired_in)
|
34
|
+
end
|
35
|
+
|
36
|
+
inventory_item.quantity_available
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
module RedisStock
|
3
|
+
class InventoryUpdater
|
4
|
+
class UnableToRestock < StandardError; end
|
5
|
+
class UnableToUnstock < StandardError; end
|
6
|
+
|
7
|
+
def initialize(line_item_ids)
|
8
|
+
@line_item_ids = line_item_ids
|
9
|
+
end
|
10
|
+
|
11
|
+
def unstock!
|
12
|
+
keys, quantities, inventory_ids = extract_inventory_data
|
13
|
+
|
14
|
+
raise UnableToUnstock, Spree.t(:insufficient_stock_lines_present) unless unstock(keys, quantities)
|
15
|
+
|
16
|
+
inventory_id_and_quantities = inventory_ids.map.with_index do |inventory_id, i|
|
17
|
+
{ inventory_id: inventory_id, quantity: -quantities[i] }
|
18
|
+
end
|
19
|
+
|
20
|
+
schedule_sync_inventory(inventory_id_and_quantities)
|
21
|
+
end
|
22
|
+
|
23
|
+
def restock!
|
24
|
+
keys, quantities, inventory_ids = extract_inventory_data
|
25
|
+
|
26
|
+
raise UnableToRestock unless restock(keys, quantities)
|
27
|
+
|
28
|
+
inventory_id_and_quantities = inventory_ids.map.with_index do |inventory_id, i|
|
29
|
+
{ inventory_id: inventory_id, quantity: quantities[i] }
|
30
|
+
end
|
31
|
+
|
32
|
+
schedule_sync_inventory(inventory_id_and_quantities)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def line_items
|
38
|
+
@line_items ||= Spree::LineItem.where(id: @line_item_ids)
|
39
|
+
end
|
40
|
+
|
41
|
+
def unstock(keys, quantities)
|
42
|
+
SpreeCmCommissioner.redis_pool.with do |redis|
|
43
|
+
redis.eval(unstock_redis_script, keys: keys, argv: quantities)
|
44
|
+
end.positive?
|
45
|
+
end
|
46
|
+
|
47
|
+
def restock(keys, quantities)
|
48
|
+
SpreeCmCommissioner.redis_pool.with do |redis|
|
49
|
+
redis.eval(restock_redis_script, keys: keys, argv: quantities)
|
50
|
+
end.positive?
|
51
|
+
end
|
52
|
+
|
53
|
+
# Return: [CachedInventoryItem(...), CachedInventoryItem(...)]
|
54
|
+
def cached_inventory_items
|
55
|
+
@cached_inventory_items ||= SpreeCmCommissioner::RedisStock::LineItemsCachedInventoryItemsBuilder.new(line_item_ids: @line_item_ids)
|
56
|
+
.call.values.flatten
|
57
|
+
end
|
58
|
+
|
59
|
+
def extract_inventory_data
|
60
|
+
keys = []
|
61
|
+
quantities = []
|
62
|
+
inventory_ids = []
|
63
|
+
|
64
|
+
cached_inventory_items.each do |cached_inventory_item|
|
65
|
+
keys << cached_inventory_item.inventory_key
|
66
|
+
quantities << line_items.find { |item| item.variant_id == cached_inventory_item.variant_id }.quantity
|
67
|
+
inventory_ids << cached_inventory_item.inventory_item_id
|
68
|
+
end
|
69
|
+
|
70
|
+
[keys, quantities, inventory_ids]
|
71
|
+
end
|
72
|
+
|
73
|
+
def unstock_redis_script
|
74
|
+
<<~LUA
|
75
|
+
local keys = KEYS
|
76
|
+
local quantities = ARGV
|
77
|
+
|
78
|
+
-- Check availability first
|
79
|
+
for i, key in ipairs(keys) do
|
80
|
+
local current = tonumber(redis.call('GET', key) or 0)
|
81
|
+
if current - tonumber(quantities[i]) < 0 then
|
82
|
+
return 0
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
-- Apply updates
|
87
|
+
for i, key in ipairs(keys) do
|
88
|
+
redis.call('DECRBY', key, tonumber(quantities[i]))
|
89
|
+
end
|
90
|
+
|
91
|
+
return 1
|
92
|
+
LUA
|
93
|
+
end
|
94
|
+
|
95
|
+
def restock_redis_script
|
96
|
+
<<~LUA
|
97
|
+
local keys = KEYS
|
98
|
+
local quantities = ARGV
|
99
|
+
|
100
|
+
-- Apply restock updates
|
101
|
+
for i, key in ipairs(keys) do
|
102
|
+
redis.call('INCRBY', key, tonumber(quantities[i]))
|
103
|
+
end
|
104
|
+
|
105
|
+
return 1
|
106
|
+
LUA
|
107
|
+
end
|
108
|
+
|
109
|
+
def schedule_sync_inventory(inventory_id_and_quantities)
|
110
|
+
SpreeCmCommissioner::InventoryItemSyncerJob.perform_later(inventory_id_and_quantities:)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/app/models/spree_cm_commissioner/redis_stock/line_items_cached_inventory_items_builder.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
module RedisStock
|
3
|
+
class LineItemsCachedInventoryItemsBuilder
|
4
|
+
attr_reader :line_item_ids
|
5
|
+
|
6
|
+
def initialize(line_item_ids:)
|
7
|
+
@line_item_ids = line_item_ids
|
8
|
+
end
|
9
|
+
|
10
|
+
# return list of inventory items group by :line_item_id:
|
11
|
+
# {
|
12
|
+
# 1: [ CachedInventoryItem(...), CachedInventoryItem(...) ],
|
13
|
+
# 2: [ CachedInventoryItem(...), CachedInventoryItem(...) ],
|
14
|
+
# }
|
15
|
+
def call
|
16
|
+
cached_inventory_items.group_by do |cached_inventory_item|
|
17
|
+
line_item = line_items.find { |item| item.variant_id == cached_inventory_item.variant_id }
|
18
|
+
line_item.id
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def cached_inventory_items
|
23
|
+
@cached_inventory_items ||= SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(inventory_items)
|
24
|
+
.call
|
25
|
+
end
|
26
|
+
|
27
|
+
def inventory_items
|
28
|
+
@inventory_items ||= line_items.flat_map do |line_item|
|
29
|
+
# TODO: N+1, we could fix but have a product_type in line item
|
30
|
+
# then include inventory_items in line item directly #2581
|
31
|
+
scope = line_item.inventory_items
|
32
|
+
scope = scope.where(inventory_date: line_item.date_range) if line_item.permanent_stock?
|
33
|
+
scope
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def line_items
|
38
|
+
@line_items ||= Spree::LineItem.where(id: line_item_ids).includes(variant: %i[product])
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
module RedisStock
|
3
|
+
class VariantCachedInventoryItemsBuilder
|
4
|
+
attr_reader :variant_id, :from_date, :to_date
|
5
|
+
|
6
|
+
def initialize(variant_id:, from_date: nil, to_date: nil)
|
7
|
+
@variant_id = variant_id
|
8
|
+
@from_date = from_date
|
9
|
+
@to_date = to_date
|
10
|
+
end
|
11
|
+
|
12
|
+
# output: [ CachedInventoryItem(...), CachedInventoryItem(...) ]
|
13
|
+
def call
|
14
|
+
::SpreeCmCommissioner::RedisStock::CachedInventoryItemsBuilder.new(inventory_items).call
|
15
|
+
end
|
16
|
+
|
17
|
+
def inventory_items
|
18
|
+
variant = Spree::Variant.find(variant_id)
|
19
|
+
|
20
|
+
inventory_items = variant.inventory_items
|
21
|
+
inventory_items.where(inventory_date: from_date..to_date) if variant.permanent_stock?
|
22
|
+
|
23
|
+
inventory_items
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|