spree_cm_commissioner 1.10.0 → 1.11.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 +131 -98
- data/.gitignore +2 -1
- data/.vscode/settings.json +1 -1
- data/Gemfile.lock +22 -1
- data/Rakefile +33 -4
- data/app/controllers/spree/admin/stock_managements_controller.rb +56 -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/controllers/spree/api/v2/storefront/queue_cart/line_items_controller.rb +2 -2
- 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 +23 -0
- data/app/interactors/spree_cm_commissioner/ensure_correct_product_type.rb +40 -0
- data/app/interactors/spree_cm_commissioner/inventory_item_syncer.rb +25 -0
- data/app/interactors/spree_cm_commissioner/stock/inventory_items_adjuster.rb +13 -0
- data/app/interactors/spree_cm_commissioner/stock/permanent_inventory_items_generator.rb +75 -0
- data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +24 -0
- data/app/interactors/spree_cm_commissioner/vattanac_bank_initiator.rb +27 -8
- data/app/jobs/spree_cm_commissioner/ensure_correct_product_type_job.rb +7 -0
- data/app/jobs/spree_cm_commissioner/inventory_item_syncer_job.rb +7 -0
- data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +11 -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_delegation.rb +1 -3
- 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 +55 -0
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +16 -5
- data/app/models/spree_cm_commissioner/order_decorator.rb +15 -0
- data/app/models/spree_cm_commissioner/place.rb +11 -2
- data/app/models/spree_cm_commissioner/product_decorator.rb +9 -2
- 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 +126 -0
- data/app/models/spree_cm_commissioner/redis_stock/line_items_cached_inventory_items_builder.rb +36 -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_item_decorator.rb +18 -0
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +11 -0
- data/app/models/spree_cm_commissioner/taxon_option_type.rb +8 -0
- data/app/models/spree_cm_commissioner/taxon_option_value.rb +8 -0
- data/app/models/spree_cm_commissioner/trip.rb +0 -11
- data/app/models/spree_cm_commissioner/trip_stop.rb +11 -4
- data/app/models/spree_cm_commissioner/variant_decorator.rb +39 -27
- data/app/models/spree_cm_commissioner/vendor_stop.rb +2 -1
- data/app/queries/spree_cm_commissioner/vendor_stop_place_query.rb +54 -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/guest_serializer.rb +1 -0
- data/app/services/spree_cm_commissioner/aes_encryption_service.rb +6 -4
- data/app/services/spree_cm_commissioner/organizer/export_guest_csv_service.rb +2 -0
- data/app/views/spree/admin/stock_managements/_events_popover.html.erb +23 -0
- data/app/views/spree/admin/stock_managements/_variant_stock_items.html.erb +3 -1
- data/app/views/spree/admin/stock_managements/calendar.html.erb +35 -0
- data/app/views/spree/admin/stock_managements/index.html.erb +40 -5
- data/config/initializers/spree_permitted_attributes.rb +5 -0
- data/config/routes.rb +11 -2
- data/db/migrate/20250304293518_create_cm_inventory_items.rb +21 -0
- data/db/migrate/20250418072528_add_nested_set_columns_to_places.rb +10 -0
- data/db/migrate/20250429094228_add_lock_version_to_cm_inventory_items.rb +5 -0
- data/db/migrate/20250430091742_create_cm_taxon_option_types.rb +9 -0
- data/db/migrate/20250430092928_create_cm_taxon_option_values.rb +9 -0
- data/db/migrate/20250502025848_add_index_to_spree_products.rb +5 -0
- data/db/migrate/20250502030001_add_product_type_to_spree_variants.rb +5 -0
- data/db/migrate/20250502030002_add_product_type_to_spree_line_items.rb +5 -0
- data/db/migrate/20250506092929_add_trip_count_to_cm_vendor_stops.rb +5 -0
- data/docker-compose.yml +1 -1
- data/lib/generators/spree_cm_commissioner/install/install_generator.rb +11 -3
- 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/homepage_section_relatable_factory.rb +1 -1
- data/lib/spree_cm_commissioner/test_helper/factories/inventory_item_factory.rb +9 -0
- data/lib/spree_cm_commissioner/test_helper/factories/line_item_factory.rb +1 -1
- data/lib/spree_cm_commissioner/test_helper/factories/place_factory.rb +11 -1
- data/lib/spree_cm_commissioner/test_helper/factories/product_factory.rb +18 -5
- data/lib/spree_cm_commissioner/test_helper/factories/stock_location_factory.rb +2 -2
- data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +39 -6
- data/lib/spree_cm_commissioner/test_helper/factories/vendor_factory.rb +1 -1
- 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/ensure_correct_product_type.rake +7 -0
- data/lib/tasks/generate_inventory_items.rake +7 -0
- data/lib/tasks/migrate_and_rebuild_place_hierarchy.rake +9 -0
- data/lib/tasks/update_orphan_root_places.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
@@ -10,6 +10,8 @@ module SpreeCmCommissioner
|
|
10
10
|
assign_prototype
|
11
11
|
create_child_taxon
|
12
12
|
build_home_banner
|
13
|
+
assign_option_types
|
14
|
+
assign_option_values
|
13
15
|
end
|
14
16
|
end
|
15
17
|
|
@@ -61,5 +63,26 @@ module SpreeCmCommissioner
|
|
61
63
|
|
62
64
|
context.fail!(message: 'Home banner upload failed') unless banner.persisted?
|
63
65
|
end
|
66
|
+
|
67
|
+
def assign_options(model_class, param_key, foreign_key)
|
68
|
+
return unless params[param_key]
|
69
|
+
|
70
|
+
params[param_key].each do |id|
|
71
|
+
record = model_class.new(
|
72
|
+
taxon_id: @parent_taxon.id,
|
73
|
+
foreign_key => id
|
74
|
+
)
|
75
|
+
|
76
|
+
context.fail!(message: record.errors.full_messages.join(', ')) unless record.save
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def assign_option_types
|
81
|
+
assign_options(SpreeCmCommissioner::TaxonOptionType, :option_type_id, :option_type_id)
|
82
|
+
end
|
83
|
+
|
84
|
+
def assign_option_values
|
85
|
+
assign_options(SpreeCmCommissioner::TaxonOptionValue, :option_value_id, :option_value_id)
|
86
|
+
end
|
64
87
|
end
|
65
88
|
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
class EnsureCorrectProductType < BaseInteractor
|
3
|
+
def call
|
4
|
+
Spree::Product
|
5
|
+
.left_joins(variants_including_master: %i[inventory_items line_items])
|
6
|
+
.where(
|
7
|
+
'spree_variants.product_type IS NULL OR
|
8
|
+
spree_variants.product_type != spree_products.product_type OR
|
9
|
+
|
10
|
+
cm_inventory_items.product_type IS NULL OR
|
11
|
+
cm_inventory_items.product_type != spree_products.product_type OR
|
12
|
+
|
13
|
+
spree_line_items.product_type IS NULL OR
|
14
|
+
spree_line_items.product_type != spree_products.product_type OR
|
15
|
+
|
16
|
+
spree_products.product_type IS NOT NULL
|
17
|
+
'
|
18
|
+
)
|
19
|
+
.distinct.find_each do |product|
|
20
|
+
sync_product_type_for(product)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def sync_product_type_for(product)
|
25
|
+
product_type = Spree::Variant.product_types[product.product_type]
|
26
|
+
|
27
|
+
product.variants_including_master
|
28
|
+
.where('spree_variants.product_type IS NULL OR spree_variants.product_type != ?', product_type)
|
29
|
+
.update_all(product_type: product_type) # rubocop:disable Rails/SkipsModelValidations
|
30
|
+
|
31
|
+
product.line_items
|
32
|
+
.where('spree_line_items.product_type IS NULL OR spree_line_items.product_type != ?', product_type)
|
33
|
+
.update_all(product_type: product_type) # rubocop:disable Rails/SkipsModelValidations
|
34
|
+
|
35
|
+
product.inventory_items
|
36
|
+
.where('cm_inventory_items.product_type IS NULL OR cm_inventory_items.product_type != ?', product_type)
|
37
|
+
.update_all(product_type: product_type) # rubocop:disable Rails/SkipsModelValidations
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
class InventoryItemSyncer < BaseInteractor
|
3
|
+
# inventory_id_and_quantities = [{ inventory_id: inventory_item1.id, quantity: 5 } ]
|
4
|
+
delegate :inventory_id_and_quantities, to: :context
|
5
|
+
|
6
|
+
def call
|
7
|
+
ActiveRecord::Base.transaction do
|
8
|
+
inventory_items.each do |inventory_item|
|
9
|
+
quantity = inventory_id_and_quantities.find { |item| item[:inventory_id] == inventory_item.id }&.dig(:quantity) || 0
|
10
|
+
adjust_quantity_available(inventory_item, quantity)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def adjust_quantity_available(inventory_item, quantity)
|
18
|
+
inventory_item.update!(quantity_available: inventory_item.quantity_available + quantity)
|
19
|
+
end
|
20
|
+
|
21
|
+
def inventory_items
|
22
|
+
@inventory_items ||= InventoryItem.where(id: inventory_id_and_quantities.pluck(:inventory_id))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
module Stock
|
3
|
+
class InventoryItemsAdjuster < BaseInteractor
|
4
|
+
delegate :variant, :quantity, to: :context
|
5
|
+
|
6
|
+
def call
|
7
|
+
variant.inventory_items.active.find_each do |inventory_item|
|
8
|
+
inventory_item.adjust_quantity!(quantity)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,75 @@
|
|
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 pre_inventory_days_for(variant)
|
9
|
+
context.pre_inventory_days || variant.pre_inventory_days
|
10
|
+
end
|
11
|
+
|
12
|
+
def call
|
13
|
+
variants.in_batches(of: variants_per_batch) do |batch|
|
14
|
+
generate_inventory_items_for_batch(batch)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def generate_inventory_items_for_batch(batch)
|
21
|
+
total_on_hand_by_variant = total_on_hand_for(batch)
|
22
|
+
batch.each do |variant|
|
23
|
+
count_on_hand = total_on_hand_by_variant[variant.id] || 0
|
24
|
+
generate_inventory_items_for_variant(variant, count_on_hand)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate_inventory_items_for_variant(variant, count_on_hand)
|
29
|
+
inventory_dates_for(variant).each do |inventory_date|
|
30
|
+
next if inventory_exist?(variant, inventory_date)
|
31
|
+
|
32
|
+
create_inventory_item(variant, inventory_date, count_on_hand)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def inventory_dates_for(variant)
|
37
|
+
start_date = Time.zone.tomorrow
|
38
|
+
end_date = Time.zone.today + pre_inventory_days_for(variant)
|
39
|
+
|
40
|
+
(start_date..end_date)
|
41
|
+
end
|
42
|
+
|
43
|
+
def inventory_exist?(variant, inventory_date)
|
44
|
+
variant.inventory_items.exists?(inventory_date: inventory_date)
|
45
|
+
end
|
46
|
+
|
47
|
+
def create_inventory_item(variant, inventory_date, count_on_hand)
|
48
|
+
variant.inventory_items.create!(
|
49
|
+
inventory_date: inventory_date,
|
50
|
+
quantity_available: count_on_hand,
|
51
|
+
max_capacity: count_on_hand,
|
52
|
+
product_type: variant.product_type
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Returns a hash: { variant_id => total_on_hand, ... }
|
57
|
+
def total_on_hand_for(variants)
|
58
|
+
variant_ids = variants.pluck(:id)
|
59
|
+
|
60
|
+
Spree::StockItem
|
61
|
+
.joins(:stock_location)
|
62
|
+
.where(deleted_at: nil, variant_id: variant_ids)
|
63
|
+
.where(spree_stock_locations: { active: true })
|
64
|
+
.group(:variant_id)
|
65
|
+
.sum(:count_on_hand)
|
66
|
+
end
|
67
|
+
|
68
|
+
def variants
|
69
|
+
scope = Spree::Variant.active.with_permanent_stock.where(is_master: false)
|
70
|
+
scope = scope.where(id: variant_ids) if variant_ids.present?
|
71
|
+
scope
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
module Stock
|
3
|
+
class StockMovementCreator < BaseInteractor
|
4
|
+
delegate :variant_id, :stock_location_id, :current_store, :stock_movement_params, to: :context
|
5
|
+
|
6
|
+
def call
|
7
|
+
variant = current_store.variants.find(variant_id)
|
8
|
+
|
9
|
+
return context.fail!(message: Spree.t(:doesnt_track_inventory)) unless variant.track_inventory?
|
10
|
+
|
11
|
+
stock_location = Spree::StockLocation.find(stock_location_id)
|
12
|
+
stock_movement = stock_location.stock_movements.build(stock_movement_params)
|
13
|
+
stock_movement.stock_item = stock_location.set_up_stock_item(variant)
|
14
|
+
|
15
|
+
if stock_movement.save
|
16
|
+
SpreeCmCommissioner::Stock::InventoryItemsAdjusterJob.perform_later(variant_id: variant.id, quantity: stock_movement.quantity)
|
17
|
+
context.stock_movement = stock_movement
|
18
|
+
else
|
19
|
+
context.fail!(message: stock_movement.errors.full_messages.join(', '))
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
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
|
@@ -57,7 +53,7 @@ module SpreeCmCommissioner
|
|
57
53
|
user_data = context.user_data
|
58
54
|
|
59
55
|
identity = SpreeCmCommissioner::UserIdentityProvider.vattanac_bank
|
60
|
-
.find_or_initialize_by(sub: user_data['
|
56
|
+
.find_or_initialize_by(sub: user_data['phoneNum'])
|
61
57
|
|
62
58
|
if identity.persisted?
|
63
59
|
context.user = identity.user
|
@@ -82,7 +78,7 @@ module SpreeCmCommissioner
|
|
82
78
|
identity = context.identity
|
83
79
|
|
84
80
|
identity.name = full_name
|
85
|
-
|
81
|
+
identity.email = user_data['email']
|
86
82
|
context.user = Spree::User.new(
|
87
83
|
first_name: user_data['firstName'],
|
88
84
|
last_name: user_data['lastName'],
|
@@ -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
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module SpreeCmCommissioner
|
2
|
+
module Stock
|
3
|
+
class InventoryItemsAdjusterJob < ApplicationUniqueJob
|
4
|
+
def perform(variant_id:, quantity:)
|
5
|
+
variant = Spree::Variant.find(variant_id)
|
6
|
+
|
7
|
+
SpreeCmCommissioner::Stock::InventoryItemsAdjuster.call(variant: variant, quantity: quantity)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
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
|
|
@@ -3,12 +3,10 @@ module SpreeCmCommissioner
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
delegate :
|
7
|
-
:subscribable?,
|
6
|
+
delegate :subscribable?,
|
8
7
|
:allowed_upload_later?,
|
9
8
|
:need_confirmation?, :need_confirmation, :kyc,
|
10
9
|
:allow_anonymous_booking,
|
11
|
-
:accommodation?, :service?, :ecommerce?,
|
12
10
|
:associated_event,
|
13
11
|
:allow_self_check_in,
|
14
12
|
:allow_self_check_in?,
|
@@ -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,55 @@
|
|
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 :active, -> { where(inventory_date: nil).or(where('inventory_date >= ?', Time.zone.today)) }
|
16
|
+
|
17
|
+
before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
|
18
|
+
|
19
|
+
def adjust_quantity!(quantity)
|
20
|
+
with_lock do
|
21
|
+
self.max_capacity = max_capacity + quantity
|
22
|
+
self.quantity_available = quantity_available + quantity
|
23
|
+
save!
|
24
|
+
|
25
|
+
# When user has been searched or booked a product, it has cached the quantity in redis,
|
26
|
+
# So we need to update redis cache if inventory key has been created in redis
|
27
|
+
adjust_quantity_in_redis(quantity)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def adjust_quantity_in_redis(quantity)
|
32
|
+
SpreeCmCommissioner.redis_pool.with do |redis|
|
33
|
+
cached_quantity_available = redis.get("inventory:#{id}")
|
34
|
+
# ignore if redis doesn't exist
|
35
|
+
return if cached_quantity_available.nil? # rubocop:disable Lint/NonLocalExitFromIterator
|
36
|
+
|
37
|
+
if quantity.positive?
|
38
|
+
redis.incrby("inventory:#{id}", quantity)
|
39
|
+
else
|
40
|
+
redis.decrby("inventory:#{id}", quantity.abs)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def active?
|
46
|
+
inventory_date.nil? || inventory_date >= Time.zone.today
|
47
|
+
end
|
48
|
+
|
49
|
+
def redis_expired_in
|
50
|
+
expired_in = 31_536_000 # 1 year for normal stock
|
51
|
+
expired_in = Time.parse(inventory_date.to_s).end_of_day.to_i - Time.zone.now.to_i if inventory_date.present?
|
52
|
+
[expired_in, 0].max
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
module SpreeCmCommissioner
|
2
2
|
module LineItemDecorator
|
3
|
-
def self.prepended(base) # rubocop:disable Metrics/MethodLength
|
3
|
+
def self.prepended(base) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
4
4
|
include_modules(base)
|
5
5
|
|
6
6
|
base.belongs_to :accepter, class_name: 'Spree::User', optional: true
|
@@ -9,6 +9,11 @@ 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
|
+
|
13
|
+
base.has_many :inventory_items, lambda { |line_item|
|
14
|
+
where(inventory_date: nil).or(where(inventory_date: line_item.date_range))
|
15
|
+
}, through: :variant
|
16
|
+
|
12
17
|
base.has_many :taxons, class_name: 'Spree::Taxon', through: :product
|
13
18
|
base.has_many :guests, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
|
14
19
|
base.has_many :pending_guests, pending_guests_query, class_name: 'SpreeCmCommissioner::Guest', dependent: :destroy
|
@@ -21,16 +26,15 @@ module SpreeCmCommissioner
|
|
21
26
|
base.validate :validate_seats_reservation, if: :transit?
|
22
27
|
|
23
28
|
base.before_create :add_due_date, if: :subscription?
|
29
|
+
base.before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
|
24
30
|
|
25
31
|
base.validate :ensure_not_exceed_max_quantity_per_order, if: -> { variant&.max_quantity_per_order.present? }
|
26
32
|
|
27
33
|
base.whitelisted_ransackable_associations |= %w[guests order]
|
28
34
|
base.whitelisted_ransackable_attributes |= %w[number to_date from_date vendor_id]
|
29
35
|
|
30
|
-
base.delegate :delivery_required?, :
|
36
|
+
base.delegate :delivery_required?, :high_demand,
|
31
37
|
to: :variant
|
32
|
-
base.delegate :discontinue_on, :product_type, :accommodation?, :service?, :ecommerce?, :need_confirmation,
|
33
|
-
to: :product
|
34
38
|
|
35
39
|
base.accepts_nested_attributes_for :guests, allow_destroy: true
|
36
40
|
base.accepts_nested_attributes_for :line_item_seats, allow_destroy: true
|
@@ -41,6 +45,10 @@ module SpreeCmCommissioner
|
|
41
45
|
json_api_columns << :vendor_id
|
42
46
|
end
|
43
47
|
|
48
|
+
def discontinue_on
|
49
|
+
variant.discontinue_on || product.discontinue_on
|
50
|
+
end
|
51
|
+
|
44
52
|
def base.search_by_qr_data!(data)
|
45
53
|
matches = data.match(/(R\d+)-([A-Za-z0-9_\-]+)-(L\d+)/)&.captures
|
46
54
|
|
@@ -59,6 +67,7 @@ module SpreeCmCommissioner
|
|
59
67
|
base.include SpreeCmCommissioner::LineItemDurationable
|
60
68
|
base.include SpreeCmCommissioner::LineItemsFilterScope
|
61
69
|
base.include SpreeCmCommissioner::LineItemGuestsConcern
|
70
|
+
base.include SpreeCmCommissioner::ProductType
|
62
71
|
base.include SpreeCmCommissioner::ProductDelegation
|
63
72
|
base.include SpreeCmCommissioner::KycBitwise
|
64
73
|
end
|
@@ -174,7 +183,7 @@ module SpreeCmCommissioner
|
|
174
183
|
|
175
184
|
# override
|
176
185
|
def sufficient_stock?
|
177
|
-
return transit_sufficient_stock? if
|
186
|
+
return transit_sufficient_stock? if transit?
|
178
187
|
|
179
188
|
SpreeCmCommissioner::Stock::LineItemAvailabilityChecker.new(self).can_supply?(quantity)
|
180
189
|
end
|
@@ -230,6 +239,8 @@ module SpreeCmCommissioner
|
|
230
239
|
end
|
231
240
|
|
232
241
|
def validate_seats_reservation
|
242
|
+
return if reservation_trip.blank?
|
243
|
+
|
233
244
|
if reservation_trip.allow_seat_selection && !selected_seats_available?
|
234
245
|
errors.add(:base, :some_seats_are_booked, message: 'Some seats are already booked')
|
235
246
|
elsif !reservation_trip.allow_seat_selection && !seat_quantity_available?(reservation_trip)
|
@@ -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(
|
@@ -2,6 +2,8 @@ require_dependency 'spree_cm_commissioner'
|
|
2
2
|
|
3
3
|
module SpreeCmCommissioner
|
4
4
|
class Place < ApplicationRecord
|
5
|
+
acts_as_nested_set
|
6
|
+
|
5
7
|
validates :reference, presence: true, if: :validate_reference?
|
6
8
|
validates :lat, presence: true, if: :validate_lat?
|
7
9
|
validates :lon, presence: true, if: :validate_lon?
|
@@ -13,15 +15,22 @@ module SpreeCmCommissioner
|
|
13
15
|
|
14
16
|
has_many :product_places, class_name: 'SpreeCmCommissioner::ProductPlace', dependent: :destroy
|
15
17
|
has_many :products, through: :product_places
|
16
|
-
has_many :children, class_name: 'SpreeCmCommissioner::Place', foreign_key: :parent_id, dependent: :destroy
|
17
|
-
belongs_to :parent, class_name: 'SpreeCmCommissioner::Place', optional: true
|
18
18
|
|
19
|
+
has_many :children, -> { order(:lft) }, class_name: 'SpreeCmCommissioner::Place', foreign_key: :parent_id, dependent: :destroy
|
19
20
|
has_many :vendor_stops, class_name: 'SpreeCmCommissioner::VendorStop', dependent: :destroy
|
20
21
|
|
21
22
|
def self.ransackable_attributes(auth_object = nil)
|
22
23
|
super & %w[name code]
|
23
24
|
end
|
24
25
|
|
26
|
+
def full_path_name
|
27
|
+
self_and_ancestors.map(&:name).reverse.join(', ')
|
28
|
+
end
|
29
|
+
|
30
|
+
def path_ids
|
31
|
+
self_and_ancestors.map(&:id)
|
32
|
+
end
|
33
|
+
|
25
34
|
def validate_reference?
|
26
35
|
Spree::Store.default.code.exclude?('billing')
|
27
36
|
end
|
@@ -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,9 +58,8 @@ module SpreeCmCommissioner
|
|
57
58
|
base.scope :subscribable, -> { where(subscribable: 1) }
|
58
59
|
|
59
60
|
base.validate :validate_event_taxons, if: -> { taxons.event.present? }
|
60
|
-
|
61
61
|
base.validate :validate_product_date, if: -> { available_on.present? && discontinue_on.present? }
|
62
|
-
|
62
|
+
base.validate :product_type_unchanged, on: :update
|
63
63
|
base.validates :commission_rate, numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: 100 }, allow_nil: true
|
64
64
|
|
65
65
|
base.whitelisted_ransackable_attributes = %w[description name slug discontinue_on status vendor_id short_name route_type]
|
@@ -102,6 +102,13 @@ module SpreeCmCommissioner
|
|
102
102
|
|
103
103
|
errors.add(:discontinue_on, 'must be after the available on date')
|
104
104
|
end
|
105
|
+
|
106
|
+
def product_type_unchanged
|
107
|
+
return if product_type_was.nil?
|
108
|
+
return unless product_type_changed?
|
109
|
+
|
110
|
+
errors.add(:product_type, 'cannot be changed once set')
|
111
|
+
end
|
105
112
|
end
|
106
113
|
end
|
107
114
|
|