spree_cm_commissioner 2.5.0.pre.pre1 → 2.5.0.pre.pre3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 766f3c96b11b0771314a8713c96706a97aaa777879fc7de568c6cde2d45a69b3
4
- data.tar.gz: 61425d946b1b97f4d56f385fa3fef1e32482d00537b5ed4de2ace0bb7a66a41d
3
+ metadata.gz: edd6f4b46f3da3028a4bfce9ea79cbf561c9ada3389da25dff55a1f5308caf05
4
+ data.tar.gz: bba75e416f319cf0cde0f34743030bf23c3f63ca243534f13632c0ebc78bc0c7
5
5
  SHA512:
6
- metadata.gz: 0f76688e91c61895340e65c38dc8defe2e92eb2d96a8011cb6064d97811b982a26102aa217b257aee237af25cc9eace91fbafa97bd3d0199da1451ea46b2089c
7
- data.tar.gz: ccb93229d83dbed6f5196b461dfa006f1be936e9a41b2a7226ba2fbbe3f63ca17425bbe5c1e22da66116ecef6f43402808e4ea1a649447c6ab5df7c5c292dd5d
6
+ metadata.gz: 712863b2c8323210e4e08a1278ea4ef0db673f73a3f8e088a3dc1a8393a3932026aa9607943869f27f0fcab69e51f38cada274226d806983ae0b0468e5ea73c6
7
+ data.tar.gz: 50f2fcbdd8aca176085f8a268e6dc85b508ea80c0d314d58ba90e231b34d7b1647b36b3c4caec6e414d51704afbf67ed2e5b6210770e29ef28cee031b40a9b73
data/Gemfile.lock CHANGED
@@ -34,7 +34,7 @@ GIT
34
34
  PATH
35
35
  remote: .
36
36
  specs:
37
- spree_cm_commissioner (2.5.0.pre.pre1)
37
+ spree_cm_commissioner (2.5.0.pre.pre3)
38
38
  activerecord-multi-tenant
39
39
  activerecord_json_validator (~> 2.1, >= 2.1.3)
40
40
  aws-sdk-cloudfront
@@ -23,10 +23,8 @@ module SpreeCmCommissioner
23
23
  private
24
24
 
25
25
  def adjust_inventory_items_async(variant_id, quantity)
26
- params = { variant_id: variant_id, quantity: quantity }
27
-
28
- CmAppLogger.log(label: "#{self.class.name}#adjust_inventory_items_async", data: params) do
29
- SpreeCmCommissioner::Stock::InventoryItemsAdjusterJob.perform_later(**params)
26
+ CmAppLogger.log(label: "#{self.class.name}#adjust_inventory_items_async", data: { variant_id: variant_id, quantity: quantity }) do
27
+ SpreeCmCommissioner::Stock::InventoryItemsAdjusterJob.perform_later(variant_id: variant_id, quantity: quantity)
30
28
  end
31
29
  end
32
30
  end
@@ -1,8 +1,7 @@
1
1
  module SpreeCmCommissioner
2
2
  module Stock
3
- class InventoryItemsAdjusterJob < ApplicationJob
4
- # :variant_id, :quantity
5
- def perform(options)
3
+ class InventoryItemsAdjusterJob < ApplicationUniqueJob
4
+ def perform(options = {})
6
5
  variant = Spree::Variant.find(options[:variant_id])
7
6
 
8
7
  SpreeCmCommissioner::Stock::InventoryItemsAdjuster.call(variant: variant, quantity: options[:quantity])
@@ -24,6 +24,9 @@ module SpreeCmCommissioner
24
24
 
25
25
  included do
26
26
  has_many :integration_mappings, as: :internal, class_name: 'SpreeCmCommissioner::IntegrationMapping', dependent: :destroy
27
+ has_many :external_wins_integration_mappings, lambda {
28
+ joins(:integration).where(integration: { conflict_strategy: :external_wins })
29
+ }, as: :internal, class_name: 'SpreeCmCommissioner::IntegrationMapping'
27
30
  end
28
31
 
29
32
  class_methods do
@@ -0,0 +1,36 @@
1
+ # Caches the integration status of a line item's variant in private_metadata.
2
+ #
3
+ # Why cache?
4
+ # Most products don't have integrations, so checking `variant.vendor.integration.present?`
5
+ # on every read would cause N+1 queries. Caching avoids this performance hit.
6
+ #
7
+ # Behavior:
8
+ # The flag is set once at line item creation and remains immutable. This preserves
9
+ # the historical integration state of the order, even if the vendor's integration
10
+ # status changes later. We don't have use cases requiring dynamic updates of this yet,
11
+ # so we keep it constant for simplicity and consistency.
12
+ module SpreeCmCommissioner
13
+ module LineItemIntegration
14
+ extend ActiveSupport::Concern
15
+
16
+ included do
17
+ before_create :set_integration_flag
18
+ end
19
+
20
+ def integration?
21
+ return false if private_metadata['integration'].nil?
22
+
23
+ ActiveModel::Type::Boolean.new.cast(private_metadata['integration'])
24
+ end
25
+
26
+ private
27
+
28
+ def set_integration_flag
29
+ return if private_metadata.key?('integration')
30
+ return if variant.nil? || variant.vendor.nil?
31
+
32
+ # We use variant.vendor instead of .vendor directly as on create, the association may not be set to line item yet.
33
+ private_metadata['integration'] ||= variant.vendor.integration.present?
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ module SpreeCmCommissioner
2
+ module OrderIntegration
3
+ # Checks if any line item in this order has an integration (i.e., is from a vendor with an integration).
4
+ # This is used to determine if inventory management operations should be delegated to an external system.
5
+ def integration?
6
+ line_items.any?(&:integration?)
7
+ end
8
+
9
+ # Attempts to unstock inventory for all integration line items in this order.
10
+ # Groups line items by vendor and delegates to the vendor's integration service.
11
+ def unstock_inventory_from_external_system!
12
+ integration_line_items = line_items.select(&:integration?)
13
+ return if integration_line_items.empty?
14
+
15
+ integration_line_items.group_by(&:vendor_id).each do |_, line_items|
16
+ integration = line_items.first&.vendor&.integration
17
+ integration.unstock_external_inventory!(self, line_items) if integration.present?
18
+ end
19
+ end
20
+
21
+ # Attempts to restock inventory for all integration line items in this order.
22
+ # Groups line items by vendor and delegates to the vendor's integration service.
23
+ def restock_inventory_from_external_system!
24
+ integration_line_items = line_items.select(&:integration?)
25
+ return if integration_line_items.empty?
26
+
27
+ integration_line_items.group_by(&:vendor_id).each do |_, line_items|
28
+ integration = line_items.first&.vendor&.integration
29
+ integration.restock_external_inventory!(self, line_items) if integration.present?
30
+ end
31
+ end
32
+ end
33
+ end
@@ -143,6 +143,7 @@ module SpreeCmCommissioner
143
143
  # The payment will be reversed in vPago gem, and `Spree::Checkout::Complete` will be called, which checks `order.reload.complete?`.
144
144
  # This is critical because if the order state is complete, the payment will be marked as paid.
145
145
 
146
+ unstock_inventory_from_external_system!
146
147
  reserve_blocks!
147
148
  unstock_inventory_in_redis!
148
149
 
@@ -161,6 +162,7 @@ module SpreeCmCommissioner
161
162
 
162
163
  def restock_inventory!
163
164
  ActiveRecord::Base.transaction do
165
+ restock_inventory_from_external_system!
164
166
  cancel_blocks!
165
167
  restock_inventory_in_redis!
166
168
  end
@@ -3,6 +3,8 @@ module SpreeCmCommissioner
3
3
  class Guest < SpreeCmCommissioner::Base # rubocop:disable Metrics/ClassLength
4
4
  include SpreeCmCommissioner::KycBitwise
5
5
  include SpreeCmCommissioner::PhoneNumberSanitizer
6
+ include SpreeCmCommissioner::StoreMetadata
7
+ include SpreeCmCommissioner::Integrations::IntegrationMappable
6
8
 
7
9
  delegate :kyc, to: :line_item, allow_nil: true
8
10
  delegate :allowed_upload_later?, to: :line_item, allow_nil: true
@@ -212,9 +214,20 @@ module SpreeCmCommissioner
212
214
  end
213
215
 
214
216
  def qr_data
217
+ return external_qr_data if external_qr_data.present?
218
+
215
219
  token
216
220
  end
217
221
 
222
+ # QR data for check-in. If external_wins? is true, use their QR data (they have their own check-in system).
223
+ # Otherwise, use our system's QR data. Only applicable to models with QR support (e.g., line_item, guest).
224
+ def external_qr_data
225
+ return nil if line_item.nil? || !line_item.integration?
226
+
227
+ mapping = external_wins_integration_mappings.first
228
+ mapping.external_qr_data if mapping.present?
229
+ end
230
+
218
231
  def current_age
219
232
  return nil if dob.nil?
220
233
 
@@ -6,7 +6,7 @@ module SpreeCmCommissioner
6
6
  enum conflict_strategy: { newest_wins: 0, internal_wins: 1, external_wins: 2, manual_resolution: 3 }
7
7
 
8
8
  belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant', optional: true
9
- belongs_to :vendor, class_name: 'Spree::Vendor', optional: false
9
+ belongs_to :vendor, class_name: 'Spree::Vendor', optional: false, inverse_of: :integration
10
10
 
11
11
  has_many :integration_mappings, class_name: 'SpreeCmCommissioner::IntegrationMapping', dependent: :destroy, inverse_of: :integration
12
12
  has_many :integration_sync_sessions, class_name: 'SpreeCmCommissioner::IntegrationSyncSession', dependent: :destroy, inverse_of: :integration
@@ -17,5 +17,13 @@ module SpreeCmCommissioner
17
17
  def sync_manager
18
18
  raise NotImplementedError, 'Subclasses must implement the sync_manager method'
19
19
  end
20
+
21
+ def restock_external_inventory!(_order, _line_items)
22
+ raise NotImplementedError, 'Subclasses must implement the restock_external_inventory! method'
23
+ end
24
+
25
+ def unstock_external_inventory!(_order, _line_items)
26
+ raise NotImplementedError, 'Subclasses must implement the unstock_external_inventory! method'
27
+ end
20
28
  end
21
29
  end
@@ -11,6 +11,10 @@ module SpreeCmCommissioner
11
11
  validates :external_id, presence: true, uniqueness: { scope: %i[integration_id internal_type internal_id date] }
12
12
  validate :validate_internal_exists, if: -> { internal_type.present? && internal_id.present? }
13
13
 
14
+ # QR data for check-in. If external_wins? is true, use their QR data (they have their own check-in system).
15
+ # Otherwise, use our system's QR data. Only applicable to map with internal that has QR support (e.g., line_item, guest).
16
+ store_public_metadata :external_qr_data, :string, default: nil
17
+
14
18
  def mark_as_archived!
15
19
  self.status = :archived
16
20
  self.last_synced_at = Time.zone.now
@@ -11,6 +11,22 @@ class SpreeCmCommissioner::Integrations::StadiumXV1 < SpreeCmCommissioner::Integ
11
11
  )
12
12
  end
13
13
 
14
+ # override
15
+ def unstock_external_inventory!(order, line_items)
16
+ result = SpreeCmCommissioner::Integrations::StadiumXV1::Inventory::UnstockInventory.new.call(
17
+ integration: self,
18
+ order: order,
19
+ line_items: line_items
20
+ )
21
+
22
+ raise SpreeCmCommissioner::Integrations::SyncError, result.error unless result.success?
23
+ end
24
+
25
+ # override
26
+ def restock_external_inventory!(_order, _line_items)
27
+ raise SpreeCmCommissioner::Integrations::SyncError, 'Ticket cannot be cancelled'
28
+ end
29
+
14
30
  def client
15
31
  SpreeCmCommissioner::Integrations::StadiumXV1::ExternalClient::Client.new(
16
32
  public_key: public_key,
@@ -72,10 +72,12 @@ module SpreeCmCommissioner
72
72
  def self.include_modules(base)
73
73
  base.include Spree::Core::NumberGenerator.new(prefix: 'L')
74
74
  base.include SpreeCmCommissioner::StoreMetadata
75
+ base.include SpreeCmCommissioner::LineItemIntegration
75
76
  base.include SpreeCmCommissioner::LineItemDurationable
76
77
  base.include SpreeCmCommissioner::LineItemsFilterScope
77
78
  base.include SpreeCmCommissioner::LineItemGuestsConcern
78
79
  base.include SpreeCmCommissioner::LineItemTransitable
80
+ base.include SpreeCmCommissioner::Integrations::IntegrationMappable
79
81
  base.include SpreeCmCommissioner::ProductType
80
82
  base.include SpreeCmCommissioner::ProductDelegation
81
83
  base.include SpreeCmCommissioner::KycBitwise
@@ -198,11 +200,21 @@ module SpreeCmCommissioner
198
200
  end
199
201
 
200
202
  def qr_data
203
+ return external_qr_data if external_qr_data.present?
201
204
  return nil if order.nil?
202
205
 
203
206
  "#{order.number}-#{order.token}-L#{id}"
204
207
  end
205
208
 
209
+ # QR data for check-in. If external_wins? is true, use their QR data (they have their own check-in system).
210
+ # Otherwise, use our system's QR data. Only applicable to models with QR support (e.g., line_item, guest).
211
+ def external_qr_data
212
+ return nil unless integration?
213
+
214
+ mapping = external_wins_integration_mappings.first
215
+ mapping.external_qr_data if mapping.present?
216
+ end
217
+
206
218
  def generate_completion_steps
207
219
  generate_completion_steps!
208
220
  true
@@ -255,7 +267,7 @@ module SpreeCmCommissioner
255
267
  end
256
268
 
257
269
  def update_vendor_id
258
- self.vendor_id = variant.vendor_id
270
+ self.vendor_id = variant&.vendor_id
259
271
  end
260
272
 
261
273
  def subscription?
@@ -3,6 +3,7 @@ module SpreeCmCommissioner
3
3
  def self.prepended(base) # rubocop:disable Metrics/MethodLength
4
4
  base.include SpreeCmCommissioner::StoreMetadata
5
5
  base.include SpreeCmCommissioner::PhoneNumberSanitizer
6
+ base.include SpreeCmCommissioner::OrderIntegration
6
7
  base.include SpreeCmCommissioner::OrderSeatable
7
8
  base.include SpreeCmCommissioner::OrderStateMachine
8
9
  base.include SpreeCmCommissioner::RouteOrderCountable
@@ -40,6 +40,9 @@ module SpreeCmCommissioner
40
40
  base.has_many :vendor_kind_option_values,
41
41
  through: :option_value_vendors, source: :option_value
42
42
 
43
+ # currently a vendor can have only one integration, but in future it can be many.
44
+ base.has_one :integration, class_name: 'SpreeCmCommissioner::Integration', dependent: :destroy, inverse_of: :vendor
45
+
43
46
  base.has_many :branches, -> { branch }, class_name: 'SpreeCmCommissioner::VendorPlace'
44
47
  base.has_many :stops, -> { stop }, class_name: 'SpreeCmCommissioner::VendorPlace'
45
48
  base.has_many :locations, -> { location }, class_name: 'SpreeCmCommissioner::VendorPlace'
@@ -0,0 +1,83 @@
1
+ class SpreeCmCommissioner::Integrations::StadiumXV1
2
+ module Inventory
3
+ class UnstockInventory
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ def call(integration:, order:, line_items:)
7
+ ApplicationRecord.transaction do
8
+ line_items.each do |line_item|
9
+ raise SpreeCmCommissioner::Integrations::SyncError, 'Invalid guests' if line_item.guests.size != line_item.quantity
10
+
11
+ sync_line_item!(integration, order, line_item)
12
+ end
13
+
14
+ success(order: order, line_items: line_items)
15
+ end
16
+ rescue SpreeCmCommissioner::Integrations::SyncError,
17
+ SpreeCmCommissioner::Integrations::ExternalClientError => e
18
+ failure(nil, e.message)
19
+ end
20
+
21
+ def sync_line_item!(integration, order, line_item)
22
+ zone_mapping = line_item.product.integration_mappings.find_by(integration_id: integration.id)
23
+ raise SpreeCmCommissioner::Integrations::SyncError, 'Integration mapping not found for product' if zone_mapping.nil?
24
+
25
+ external_zone_id = zone_mapping.external_id
26
+ raise SpreeCmCommissioner::Integrations::SyncError, 'zone_id is required' if external_zone_id.blank?
27
+
28
+ external_match_id = zone_mapping.external_payload&.dig('match_id')
29
+ raise SpreeCmCommissioner::Integrations::SyncError, 'match_id is required' if external_match_id.blank?
30
+
31
+ external_club_id = zone_mapping.external_payload&.dig('club_id')
32
+ raise SpreeCmCommissioner::Integrations::SyncError, 'club_id is required' if external_club_id.blank?
33
+
34
+ tickets = integration.client.create_tickets!(
35
+ match_id: external_match_id,
36
+ user_id: order.number,
37
+ club_id: external_club_id,
38
+ zone_id: external_zone_id,
39
+ quantity: line_item.quantity
40
+ )
41
+
42
+ # External system did not create the correct number of tickets, we cannot proceed as it would lead to data inconsistency between systems.
43
+ # We raise an error to rollback the transaction & we can manually investigate the issue.
44
+ if tickets.size != line_item.quantity
45
+ raise SpreeCmCommissioner::Integrations::SyncError,
46
+ "Created tickets count (#{tickets.size}) does not match line item quantity (#{line_item.quantity})"
47
+ end
48
+
49
+ sync_guests!(line_item.guests, tickets, integration)
50
+
51
+ line_item_mapping = line_item.integration_mappings.find_or_initialize_by(
52
+ integration: integration,
53
+ external_id: tickets.map(&:_id).sort.join(',')
54
+ )
55
+ line_item_mapping.external_qr_data = group_ticket_qr_data(tickets)
56
+ line_item_mapping.mark_as_active!(external_payload: {})
57
+ end
58
+
59
+ def sync_guests!(guests, tickets, integration)
60
+ guests.each_with_index do |guest, index|
61
+ ticket = tickets[index]
62
+ next if ticket.blank?
63
+
64
+ guest_mapping = guest.integration_mappings.find_or_initialize_by(
65
+ external_id: ticket._id,
66
+ integration: integration
67
+ )
68
+
69
+ guest_mapping.external_qr_data = ticket._id
70
+ guest_mapping.mark_as_active!(external_payload: ticket.to_h)
71
+ end
72
+ end
73
+
74
+ def group_ticket_qr_data(tickets)
75
+ {
76
+ match_id: tickets.first.match_id,
77
+ ticket_ids: tickets.map(&:_id).sort,
78
+ is_online: true
79
+ }.to_json
80
+ end
81
+ end
82
+ end
83
+ end
@@ -1,5 +1,5 @@
1
1
  module SpreeCmCommissioner
2
- VERSION = '2.5.0-pre1'.freeze
2
+ VERSION = '2.5.0-pre3'.freeze
3
3
 
4
4
  module_function
5
5
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_cm_commissioner
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.5.0.pre.pre1
4
+ version: 2.5.0.pre.pre3
5
5
  platform: ruby
6
6
  authors:
7
7
  - You
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-12-12 00:00:00.000000000 Z
11
+ date: 2025-12-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: spree
@@ -1378,11 +1378,13 @@ files:
1378
1378
  - app/models/concerns/spree_cm_commissioner/kyc_bitwise.rb
1379
1379
  - app/models/concerns/spree_cm_commissioner/line_item_durationable.rb
1380
1380
  - app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb
1381
+ - app/models/concerns/spree_cm_commissioner/line_item_integration.rb
1381
1382
  - app/models/concerns/spree_cm_commissioner/line_item_transitable.rb
1382
1383
  - app/models/concerns/spree_cm_commissioner/line_items_filter_scope.rb
1383
1384
  - app/models/concerns/spree_cm_commissioner/metafield.rb
1384
1385
  - app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb
1385
1386
  - app/models/concerns/spree_cm_commissioner/option_value_attr_type.rb
1387
+ - app/models/concerns/spree_cm_commissioner/order_integration.rb
1386
1388
  - app/models/concerns/spree_cm_commissioner/order_scopes.rb
1387
1389
  - app/models/concerns/spree_cm_commissioner/order_seatable.rb
1388
1390
  - app/models/concerns/spree_cm_commissioner/order_state_machine.rb
@@ -2005,6 +2007,7 @@ files:
2005
2007
  - app/services/spree_cm_commissioner/integrations/polling.rb
2006
2008
  - app/services/spree_cm_commissioner/integrations/polling_scheduler.rb
2007
2009
  - app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb
2010
+ - app/services/spree_cm_commissioner/integrations/stadium_x_v1/inventory/unstock_inventory.rb
2008
2011
  - app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb
2009
2012
  - app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb
2010
2013
  - app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb