spree_cm_commissioner 2.4.2 → 2.5.0.pre.pre1
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 +1 -1
- data/.tool-versions +1 -1
- data/Gemfile.lock +1 -1
- data/README.md +32 -0
- data/app/controllers/spree/admin/integration_mappings_controller.rb +21 -0
- data/app/controllers/spree/admin/integration_sessions_controller.rb +21 -0
- data/app/controllers/spree/admin/integrations_controller.rb +83 -0
- data/app/controllers/spree/api/v2/storefront/event_matches_controller.rb +15 -0
- data/app/controllers/spree/api/v2/storefront/tenants_controller.rb +30 -0
- data/app/errors/spree_cm_commissioner/integrations/external_client_error.rb +10 -0
- data/app/errors/spree_cm_commissioner/integrations/sync_error.rb +4 -0
- data/app/finders/spree_cm_commissioner/events/find_matches.rb +15 -0
- data/app/helpers/spree_cm_commissioner/external_integrations_helper.rb +58 -0
- data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
- data/app/interactors/spree_cm_commissioner/stock/stock_movement_creator.rb +4 -2
- data/app/jobs/spree_cm_commissioner/integrations/base_job.rb +39 -0
- data/app/jobs/spree_cm_commissioner/integrations/polling_job.rb +53 -0
- data/app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb +30 -0
- data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +5 -4
- data/app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb +17 -0
- data/app/models/concerns/spree_cm_commissioner/integrations/integration_mappable.rb +58 -0
- data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +15 -3
- data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +14 -2
- data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +2 -0
- data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +1 -7
- data/app/models/spree_cm_commissioner/integration.rb +21 -0
- data/app/models/spree_cm_commissioner/integration_mapping.rb +37 -0
- data/app/models/spree_cm_commissioner/integration_sync_session.rb +15 -0
- data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +21 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +5 -1
- data/app/models/spree_cm_commissioner/option_type_decorator.rb +8 -0
- data/app/models/spree_cm_commissioner/option_value_decorator.rb +34 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +3 -2
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
- data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +1 -0
- data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +13 -1
- data/app/models/spree_cm_commissioner/variant_decorator.rb +7 -4
- data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
- data/app/overrides/spree/admin/shared/sub_menu/_integrations/external_integrations.html.erb.deface +6 -0
- data/app/presenters/spree/variants/{visable_options_presenter.rb → visible_options_presenter.rb} +2 -4
- data/app/serializers/spree/v2/storefront/tenant_serializer.rb +14 -0
- data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +69 -0
- data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +183 -0
- data/app/services/spree_cm_commissioner/integrations/polling.rb +70 -0
- data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +79 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +152 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +113 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +215 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb +20 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/league.rb +19 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/match.rb +95 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket.rb +81 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket_image.rb +19 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/zone.rb +90 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +35 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +38 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +44 -0
- data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +16 -0
- data/app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb +49 -0
- data/app/views/spree/admin/integration_mappings/_integration_mappings.html.erb +107 -0
- data/app/views/spree/admin/integration_mappings/index.html.erb +33 -0
- data/app/views/spree/admin/integration_sessions/_integration_sync_sessions.html.erb +116 -0
- data/app/views/spree/admin/integration_sessions/index.html.erb +42 -0
- data/app/views/spree/admin/integrations/_form.html.erb +104 -0
- data/app/views/spree/admin/integrations/_stadium_x_v1_fields.html.erb +29 -0
- data/app/views/spree/admin/integrations/edit.html.erb +45 -0
- data/app/views/spree/admin/integrations/index.html.erb +75 -0
- data/app/views/spree/admin/integrations/new.html.erb +25 -0
- data/app/views/spree/admin/tenants/_form.html.erb +79 -36
- data/config/locales/en.yml +8 -0
- data/config/locales/km.yml +8 -0
- data/config/routes.rb +9 -1
- data/db/migrate/20251017094845_create_cm_integrations.rb +22 -0
- data/db/migrate/20251017101555_create_cm_integration_sync_sessions.rb +68 -0
- data/db/migrate/20251017101605_create_cm_integration_mappings.rb +52 -0
- data/lib/cm_app_logger.rb +1 -0
- data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +25 -0
- data/lib/spree_cm_commissioner/test_helper/factories/integration_mapping_factory.rb +14 -0
- data/lib/spree_cm_commissioner/test_helper/factories/integration_sync_session_factory.rb +7 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +8 -7
- metadata +58 -5
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
|
|
2
|
+
<!-- ======================== Tenant Basic Info ======================== -->
|
|
3
|
+
<div class="card mb-4">
|
|
4
|
+
<div class="card-body">
|
|
5
|
+
<div data-hook="admin_tenant_form_fields" class="row">
|
|
6
|
+
<!-- Basic Info Section -->
|
|
7
|
+
<div class="col-12 mb-4">
|
|
8
|
+
<h4>Basic Information <span class="text-muted" style="font-size: 1rem; font-weight: normal;">(General details about the tenant)</span></h4>
|
|
9
|
+
</div>
|
|
3
10
|
<div class="col-6">
|
|
4
11
|
<%= f.field_container :name do %>
|
|
5
12
|
<%= f.label :name, raw(Spree.t(:name) + required_span_tag) %>
|
|
@@ -9,8 +16,6 @@
|
|
|
9
16
|
<% end %>
|
|
10
17
|
<% end %>
|
|
11
18
|
</div>
|
|
12
|
-
|
|
13
|
-
<!-- Slug Field -->
|
|
14
19
|
<div class="col-6">
|
|
15
20
|
<%= f.field_container :slug do %>
|
|
16
21
|
<%= f.label :slug, raw(Spree.t(:slug)) %>
|
|
@@ -20,8 +25,6 @@
|
|
|
20
25
|
<% end %>
|
|
21
26
|
<% end %>
|
|
22
27
|
</div>
|
|
23
|
-
|
|
24
|
-
<!-- Description Field -->
|
|
25
28
|
<div class="col-12">
|
|
26
29
|
<%= f.field_container :description do %>
|
|
27
30
|
<%= f.label :description, raw(Spree.t(:description)) %>
|
|
@@ -31,8 +34,6 @@
|
|
|
31
34
|
<% end %>
|
|
32
35
|
<% end %>
|
|
33
36
|
</div>
|
|
34
|
-
|
|
35
|
-
<!-- Host Field -->
|
|
36
37
|
<div class="col-12">
|
|
37
38
|
<%= f.field_container :host do %>
|
|
38
39
|
<%= f.label :host, raw(Spree.t(:host) + required_span_tag) %>
|
|
@@ -42,8 +43,37 @@
|
|
|
42
43
|
<% end %>
|
|
43
44
|
<% end %>
|
|
44
45
|
</div>
|
|
46
|
+
<div class="col-6">
|
|
47
|
+
<%= f.field_container :state do %>
|
|
48
|
+
<%= f.label :state, raw(Spree.t(:state)) %>
|
|
49
|
+
<%= f.select :state,
|
|
50
|
+
options_for_select([["Enabled", "enabled"], ["Disabled", "disabled"]], f.object&.state),
|
|
51
|
+
{ required: true },
|
|
52
|
+
class: 'select2 form-control' %>
|
|
53
|
+
<% if f.object.errors[:state].any? %>
|
|
54
|
+
<div class="error text-danger"><%= f.object.errors[:state].join(', ') %></div>
|
|
55
|
+
<% end %>
|
|
56
|
+
<% end %>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
45
61
|
|
|
46
|
-
|
|
62
|
+
<!-- ======================== Branding & App Links ======================== -->
|
|
63
|
+
<div class="card mb-4">
|
|
64
|
+
<div class="card-body">
|
|
65
|
+
<div data-hook="admin_tenant_form_fields" class="row mt-4">
|
|
66
|
+
<!-- Branding Section -->
|
|
67
|
+
<div class="col-12 mb-4">
|
|
68
|
+
<h4>Branding & App Links <span class="text-muted" style="font-size: 1rem; font-weight: normal;">(Brand colors and mobile app association settings)</span></h4>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="col-md-6">
|
|
71
|
+
<%= f.label :preferred_brand_primary_color, Spree.t(:brand_primary_color), class: 'form-label' %>
|
|
72
|
+
<%= f.text_field :preferred_brand_primary_color, class: 'form-control color-picker', placeholder: '#FF5733', value: @object.preferred_brand_primary_color %>
|
|
73
|
+
<div class="color-preview" style="margin-top: 10px;">
|
|
74
|
+
<%= content_tag :div, nil, style: "background-color: #{@object.preferred_brand_primary_color}; width: 60px; height: 60px; border: 1px solid #ccc; box-sizing: border-box; display: block;" %>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
47
77
|
<div class="col-12">
|
|
48
78
|
<%= f.field_container :preferred_assetlinks do %>
|
|
49
79
|
<%= f.label :preferred_assetlinks, raw(Spree.t(:assetlinks)) %>
|
|
@@ -69,8 +99,6 @@
|
|
|
69
99
|
<% end %>
|
|
70
100
|
<% end %>
|
|
71
101
|
</div>
|
|
72
|
-
|
|
73
|
-
<!-- Apple App Site Association Field -->
|
|
74
102
|
<div class="col-12">
|
|
75
103
|
<%= f.field_container :preferred_apple_app_site_association do %>
|
|
76
104
|
<%= f.label :preferred_apple_app_site_association, raw(Spree.t(:apple_app_site_association)) %>
|
|
@@ -100,32 +128,18 @@
|
|
|
100
128
|
<% end %>
|
|
101
129
|
<% end %>
|
|
102
130
|
</div>
|
|
103
|
-
|
|
104
|
-
<!-- Brand Primary Color Field -->
|
|
105
|
-
<div class="col-md-6">
|
|
106
|
-
<%= f.label :preferred_brand_primary_color, Spree.t(:brand_primary_color), class: 'form-label' %>
|
|
107
|
-
<%= f.text_field :preferred_brand_primary_color, class: 'form-control color-picker', placeholder: '#FF5733', value: @object.preferred_brand_primary_color %>
|
|
108
|
-
<div class="color-preview" style="margin-top: 10px;">
|
|
109
|
-
<%= content_tag :div, nil, style: "background-color: #{@object.preferred_brand_primary_color}; width: 60px; height: 60px; border: 1px solid #ccc; box-sizing: border-box; display: block;" %>
|
|
110
131
|
</div>
|
|
111
132
|
</div>
|
|
112
|
-
|
|
113
|
-
<!-- State Field -->
|
|
114
|
-
<div class="col-6">
|
|
115
|
-
<%= f.field_container :state do %>
|
|
116
|
-
<%= f.label :state, raw(Spree.t(:state)) %>
|
|
117
|
-
<%= f.select :state,
|
|
118
|
-
options_for_select([['Enabled', 'enabled'], ['Disabled', 'disabled']], f.object&.state),
|
|
119
|
-
{ required: true },
|
|
120
|
-
class: 'select2 form-control' %>
|
|
121
|
-
<% if f.object.errors[:state].any? %>
|
|
122
|
-
<div class="error text-danger"><%= f.object.errors[:state].join(', ') %></div>
|
|
123
|
-
<% end %>
|
|
124
|
-
<% end %>
|
|
125
|
-
</div>
|
|
126
133
|
</div>
|
|
127
134
|
|
|
128
|
-
|
|
135
|
+
<!-- ======================== Payment Icons ======================== -->
|
|
136
|
+
<div class="card mb-4">
|
|
137
|
+
<div class="card-body">
|
|
138
|
+
<div data-hook="admin_tenant_form_fields" class="row mt-4">
|
|
139
|
+
<!-- Payment Icons Section -->
|
|
140
|
+
<div class="col-12 mb-4">
|
|
141
|
+
<h4>Payment Icons <span class="text-muted" style="font-size: 1rem; font-weight: normal;">(Customize payment status icons for tenant checkout experience)</span></h4>
|
|
142
|
+
</div>
|
|
129
143
|
<div class="col-12">
|
|
130
144
|
<div class="row gy-4">
|
|
131
145
|
<% payment_icon_fields = [
|
|
@@ -134,7 +148,6 @@
|
|
|
134
148
|
{ field: :preferred_payment_success_image, label: Spree.t(:payment_success_image), target: 'success' },
|
|
135
149
|
{ field: :preferred_payment_loader, label: Spree.t(:payment_loader), target: 'loader' }
|
|
136
150
|
] %>
|
|
137
|
-
|
|
138
151
|
<% payment_icon_fields.each do |icon| %>
|
|
139
152
|
<div class="col-md-3">
|
|
140
153
|
<%= f.field_container icon[:field] do %>
|
|
@@ -142,7 +155,6 @@
|
|
|
142
155
|
<label class="form-label fw-bold mb-2" for="tenant_<%= icon[:field] %>">
|
|
143
156
|
<%= icon[:label] %>
|
|
144
157
|
</label>
|
|
145
|
-
|
|
146
158
|
<div class="icon-preview-container mb-2" id="icon-preview-container-<%= icon[:field] %>">
|
|
147
159
|
<span class="payment-icon-preview" id="icon-preview-<%= icon[:field] %>">
|
|
148
160
|
<% if f.object.send(icon[:field]).present? %>
|
|
@@ -154,7 +166,6 @@
|
|
|
154
166
|
<span class="remove-icon-btn <%= 'hidden' unless f.object.send(icon[:field]).present? %>" data-target="<%= icon[:field] %>">×</span>
|
|
155
167
|
</div>
|
|
156
168
|
</div>
|
|
157
|
-
|
|
158
169
|
<%= f.collection_select icon[:field],
|
|
159
170
|
@vector_icons, :path, :path,
|
|
160
171
|
{ include_blank: 'None' },
|
|
@@ -167,4 +178,36 @@
|
|
|
167
178
|
<% end %>
|
|
168
179
|
</div>
|
|
169
180
|
</div>
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<!-- ======================== Redirect Settings ======================== -->
|
|
186
|
+
<div class="card mb-4">
|
|
187
|
+
<div class="card-body">
|
|
188
|
+
<div data-hook="admin_tenant_form_fields" class="row mt-4">
|
|
189
|
+
<!-- Redirect Settings Section -->
|
|
190
|
+
<div class="col-12 mb-4">
|
|
191
|
+
<h4>Redirect Settings <span class="text-muted" style="font-size: 1rem; font-weight: normal;">(Configure host redirection and excluded paths for this tenant)</span></h4>
|
|
192
|
+
</div>
|
|
193
|
+
<div class="col-6">
|
|
194
|
+
<%= f.field_container :preferred_redirect_target_host do %>
|
|
195
|
+
<%= f.label :preferred_redirect_target_host, raw(Spree.t(:redirect_target_host)) %>
|
|
196
|
+
<%= f.text_field :preferred_redirect_target_host, class: 'form-control', placeholder: 'target.example.com' %>
|
|
197
|
+
<% if f.object.errors[:preferred_redirect_target_host].any? %>
|
|
198
|
+
<div class="error text-danger"><%= f.object.errors[:preferred_redirect_target_host].join(', ') %></div>
|
|
199
|
+
<% end %>
|
|
200
|
+
<% end %>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="col-12">
|
|
203
|
+
<%= f.field_container :preferred_redirect_excluded_paths do %>
|
|
204
|
+
<%= f.label :preferred_redirect_excluded_paths, raw(Spree.t(:redirect_excluded_paths)) %>
|
|
205
|
+
<%= f.text_area :preferred_redirect_excluded_paths, class: 'form-control', placeholder: 'eg. /vpago_payments,/.well-known' %>
|
|
206
|
+
<% if f.object.errors[:preferred_redirect_excluded_paths].any? %>
|
|
207
|
+
<div class="error text-danger"><%= f.object.errors[:preferred_redirect_excluded_paths].join(', ') %></div>
|
|
208
|
+
<% end %>
|
|
209
|
+
<% end %>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
170
213
|
</div>
|
data/config/locales/en.yml
CHANGED
|
@@ -454,6 +454,14 @@ en:
|
|
|
454
454
|
admin:
|
|
455
455
|
display_on:
|
|
456
456
|
frontend_for_early_adopter: "Storefront for Early Adopter"
|
|
457
|
+
external_integrations_title: "External Integrations"
|
|
458
|
+
integration_sync_enqueued: "Integration sync has been enqueued"
|
|
459
|
+
integration_inactive: "Integration is inactive"
|
|
460
|
+
external_integrations:
|
|
461
|
+
view_result: "View Result"
|
|
462
|
+
sync_result: "Sync Result"
|
|
463
|
+
external_payload: "External Payload"
|
|
464
|
+
no_results: "No results found"
|
|
457
465
|
event:
|
|
458
466
|
check_in:
|
|
459
467
|
success: "Guest check-in in successfully"
|
data/config/locales/km.yml
CHANGED
|
@@ -341,6 +341,14 @@ km:
|
|
|
341
341
|
admin:
|
|
342
342
|
display_on:
|
|
343
343
|
frontend_for_early_adopter: "Storefront for Early Adopter"
|
|
344
|
+
external_integrations_title: "ការភ្ជាប់ខាងក្រៅ"
|
|
345
|
+
integration_sync_enqueued: "ការធ្វើសមកាលកម្មបានដាក់ក្នុងជួរ"
|
|
346
|
+
integration_inactive: "ការភ្ជាប់មិនដំណើរការ"
|
|
347
|
+
external_integrations:
|
|
348
|
+
view_result: "មើលលទ្ធផល"
|
|
349
|
+
sync_result: "លទ្ធផលធ្វើសមកាលកម្ម"
|
|
350
|
+
external_payload: "ទិន្នន័យខាងក្រៅ"
|
|
351
|
+
no_results: "មិនមានលទ្ធផលទេ"
|
|
344
352
|
event:
|
|
345
353
|
check_in:
|
|
346
354
|
success: "Guest check-in in successfully"
|
data/config/routes.rb
CHANGED
|
@@ -283,6 +283,13 @@ Spree::Core::Engine.add_routes do
|
|
|
283
283
|
post :set_webhook
|
|
284
284
|
end
|
|
285
285
|
end
|
|
286
|
+
resources :integrations do
|
|
287
|
+
member do
|
|
288
|
+
post :enqueue_polling
|
|
289
|
+
end
|
|
290
|
+
resources :sessions, only: [:index], controller: 'integration_sessions'
|
|
291
|
+
resources :mappings, only: [:index], controller: 'integration_mappings'
|
|
292
|
+
end
|
|
286
293
|
|
|
287
294
|
resources :webhooks_subscribers do
|
|
288
295
|
resources :rules, controller: :webhooks_subscriber_rules, except: %i[index show]
|
|
@@ -579,7 +586,7 @@ Spree::Core::Engine.add_routes do
|
|
|
579
586
|
resource :cart, controller: :cart, only: %i[show create destroy] do
|
|
580
587
|
patch :restart_checkout_flow
|
|
581
588
|
end
|
|
582
|
-
|
|
589
|
+
resources :tenants, only: %i[show], constraints: { id: /.+/ }
|
|
583
590
|
resources :trip_places
|
|
584
591
|
resources :trip_search, only: [:index]
|
|
585
592
|
resources :trips, only: %i[show]
|
|
@@ -695,6 +702,7 @@ Spree::Core::Engine.add_routes do
|
|
|
695
702
|
|
|
696
703
|
resources :seat_layouts, only: %i[show index]
|
|
697
704
|
resources :inventory_items, only: %i[index]
|
|
705
|
+
resources :event_matches, only: %i[index]
|
|
698
706
|
namespace :transit do
|
|
699
707
|
resources :draft_orders, only: %i[create]
|
|
700
708
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
class CreateCmIntegrations < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :cm_integrations, if_not_exists: true do |t|
|
|
4
|
+
t.string :type, null: false
|
|
5
|
+
t.string :name, null: false
|
|
6
|
+
|
|
7
|
+
t.integer :status, null: false, default: 0
|
|
8
|
+
t.integer :conflict_strategy, null: false, default: 0
|
|
9
|
+
|
|
10
|
+
t.integer :incremental_sync_interval_seconds, null: false, default: 10
|
|
11
|
+
t.integer :full_sync_interval_hours, null: false, default: 24
|
|
12
|
+
|
|
13
|
+
t.references :tenant, foreign_key: { to_table: :cm_tenants }, null: true
|
|
14
|
+
t.references :vendor, foreign_key: { to_table: :spree_vendors }, null: false
|
|
15
|
+
|
|
16
|
+
t.jsonb :public_metadata, default: {}
|
|
17
|
+
t.jsonb :private_metadata, default: {}
|
|
18
|
+
|
|
19
|
+
t.timestamps
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
class CreateCmIntegrationSyncSessions < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :cm_integration_sync_sessions, if_not_exists: true do |t|
|
|
4
|
+
t.integer :status, null: false
|
|
5
|
+
t.integer :sync_type, null: false
|
|
6
|
+
|
|
7
|
+
t.datetime :started_at
|
|
8
|
+
t.datetime :ended_at
|
|
9
|
+
|
|
10
|
+
t.references :integration, foreign_key: { to_table: :cm_integrations }, null: false
|
|
11
|
+
t.references :tenant, foreign_key: { to_table: :cm_tenants }, null: true
|
|
12
|
+
|
|
13
|
+
# Stores the serialized sync result (SyncResult#to_h) for this session.
|
|
14
|
+
# This field is used to persist the outcome of the sync operation,
|
|
15
|
+
# allowing for detailed tracking of results, whether the sync was successful or not.
|
|
16
|
+
#
|
|
17
|
+
# Base class: app/services/spree_cm_commissioner/integrations/base/sync_result.rb
|
|
18
|
+
#
|
|
19
|
+
# Example implementations include:
|
|
20
|
+
# - Full sync results: integrations/stadium_x_v1/sync_results/full_sync_result.rb
|
|
21
|
+
# - Incremental sync results: integrations/stadium_x_v1/sync_results/incremental_sync_result.rb
|
|
22
|
+
#
|
|
23
|
+
# The result is saved upon completion of the session in the SyncManager#run_execution method:
|
|
24
|
+
# - app/services/spree_cm_commissioner/integrations/base/sync_manager.rb
|
|
25
|
+
t.jsonb :sync_result, default: {}
|
|
26
|
+
|
|
27
|
+
t.timestamps
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Indexes for PollingScheduler queries (optimized for performance)
|
|
31
|
+
# Initially used in polling_scheduler.rb
|
|
32
|
+
#
|
|
33
|
+
# Index 1: Find in_progress/pending syncs created within last hour
|
|
34
|
+
# Used by: should_run_full_sync? and should_run_incremental_sync?
|
|
35
|
+
# Query: .where(sync_type: [...], status: [...]).where('created_at > ?', 1.hour.ago).exists?
|
|
36
|
+
# Why: Composite index on all filter columns for fast lookup of active syncs
|
|
37
|
+
unless index_exists?(:cm_integration_sync_sessions, [:integration_id, :sync_type, :status, :created_at], name: 'idx_sync_sessions_integration_type_status_created_at')
|
|
38
|
+
add_index :cm_integration_sync_sessions, [:integration_id, :sync_type, :status, :created_at],
|
|
39
|
+
name: 'idx_sync_sessions_integration_type_status_created_at'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Index 2: Find latest completed full sync (partial index - only completed rows)
|
|
43
|
+
# Used by: should_run_full_sync?
|
|
44
|
+
# Query: .where(sync_type: :full, status: :completed).order(created_at: :desc).first
|
|
45
|
+
# Why: Partial index reduces size by ~60% (excludes in_progress/pending/failed rows)
|
|
46
|
+
# Descending order on created_at allows DB to return latest without sorting
|
|
47
|
+
# Only 2 columns needed since sync_type and status are filtered by WHERE clause
|
|
48
|
+
unless index_exists?(:cm_integration_sync_sessions, [:integration_id, :created_at], name: 'idx_sync_sessions_full_completed')
|
|
49
|
+
add_index :cm_integration_sync_sessions, [:integration_id, :created_at],
|
|
50
|
+
name: 'idx_sync_sessions_full_completed',
|
|
51
|
+
where: "sync_type = 0 AND status = 2", # full=0, completed=2
|
|
52
|
+
order: { created_at: :desc }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Index 3: Find latest completed incremental sync (partial index - only completed rows)
|
|
56
|
+
# Used by: should_run_incremental_sync?
|
|
57
|
+
# Query: .where(sync_type: :incremental, status: :completed).order(created_at: :desc).first
|
|
58
|
+
# Why: Partial index reduces size by ~60% (excludes in_progress/pending/failed rows)
|
|
59
|
+
# Descending order on created_at allows DB to return latest without sorting
|
|
60
|
+
# Only 2 columns needed since sync_type and status are filtered by WHERE clause
|
|
61
|
+
unless index_exists?(:cm_integration_sync_sessions, [:integration_id, :created_at], name: 'idx_sync_sessions_incremental_completed')
|
|
62
|
+
add_index :cm_integration_sync_sessions, [:integration_id, :created_at],
|
|
63
|
+
name: 'idx_sync_sessions_incremental_completed',
|
|
64
|
+
where: "sync_type = 1 AND status = 2", # incremental=1, completed=2
|
|
65
|
+
order: { created_at: :desc }
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
class CreateCmIntegrationMappings < ActiveRecord::Migration[7.0]
|
|
2
|
+
def change
|
|
3
|
+
create_table :cm_integration_mappings, if_not_exists: true do |t|
|
|
4
|
+
t.string :external_id, null: false
|
|
5
|
+
t.string :internal_type, null: false
|
|
6
|
+
t.bigint :internal_id, null: false
|
|
7
|
+
t.jsonb :external_payload, default: nil
|
|
8
|
+
|
|
9
|
+
# optional for record that can have different mappings on different dates
|
|
10
|
+
t.date :date, null: true
|
|
11
|
+
t.integer :status, null: false, default: 0
|
|
12
|
+
t.references :integration, null: false, foreign_key: { to_table: :cm_integrations }
|
|
13
|
+
|
|
14
|
+
t.jsonb :public_metadata, default: {}
|
|
15
|
+
t.jsonb :private_metadata, default: {}
|
|
16
|
+
|
|
17
|
+
t.datetime :last_synced_at
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Index 1: Unique constraint for external_id mappings
|
|
22
|
+
# Used by: IntegrationMapping#validates :external_id
|
|
23
|
+
# Query: validates :external_id, uniqueness: { scope: %i[integration_id internal_type internal_id date] }
|
|
24
|
+
# Why: Ensures a single external_id maps to only one internal record per integration/date combination
|
|
25
|
+
# Prevents duplicate mappings that could cause sync conflicts
|
|
26
|
+
# Unique index enforces this at database level for data integrity
|
|
27
|
+
unless index_exists?(:cm_integration_mappings, [:integration_id, :external_id, :internal_type, :internal_id, :date], name: 'idx_external_internal_mapping')
|
|
28
|
+
add_index :cm_integration_mappings, [:integration_id, :external_id, :internal_type, :internal_id, :date], unique: true, name: 'idx_external_internal_mapping'
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Index 2: Efficient lookup by integration_id and external_id
|
|
32
|
+
# Used by: IntegrationMappable#find_by_integration, IntegrationMappable#find_or_initialize_by_integration,
|
|
33
|
+
# IntegrationMappable#find_or_initialize_by_integration_with_mapping
|
|
34
|
+
#
|
|
35
|
+
# Query: .find_by(integration_id: integration_id, external_id: external_id)
|
|
36
|
+
# Why: Composite index allows efficient lookup by both integration_id and external_id
|
|
37
|
+
# These methods are called frequently during sync operations
|
|
38
|
+
unless index_exists?(:cm_integration_mappings, [:integration_id, :internal_type, :external_id], name: 'idx_integration_external_id')
|
|
39
|
+
add_index :cm_integration_mappings, [:integration_id, :internal_type, :external_id], name: 'idx_integration_external_id'
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Index 3: Incremental sync query optimization
|
|
43
|
+
# Used by: IntegrationMappable#find_oldest_active_mapping (called by IncrementalSyncStrategy every 10 seconds)
|
|
44
|
+
# Query: .where(integration_id: integration_id, internal_type: model_name).active.order(:last_synced_at).first
|
|
45
|
+
# Why: Composite index filters by integration_id, internal_type, status efficiently
|
|
46
|
+
# Includes last_synced_at for ORDER BY to avoid expensive sort operation
|
|
47
|
+
# Runs every 10s so performance is critical - even small optimizations matter at this frequency
|
|
48
|
+
unless index_exists?(:cm_integration_mappings, [:integration_id, :internal_type, :status, :last_synced_at], name: 'idx_incremental_sync')
|
|
49
|
+
add_index :cm_integration_mappings, [:integration_id, :internal_type, :status, :last_synced_at], name: 'idx_incremental_sync'
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
data/lib/cm_app_logger.rb
CHANGED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :cm_integration, class: SpreeCmCommissioner::Integration do
|
|
3
|
+
type { 'SpreeCmCommissioner::Integrations::StadiumXV1' }
|
|
4
|
+
name { FFaker::Company.name }
|
|
5
|
+
status { :active }
|
|
6
|
+
conflict_strategy { :newest_wins }
|
|
7
|
+
incremental_sync_interval_seconds { 300 }
|
|
8
|
+
full_sync_interval_hours { 24 }
|
|
9
|
+
association :vendor, factory: :cm_vendor
|
|
10
|
+
|
|
11
|
+
trait :active do
|
|
12
|
+
status { :active }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
trait :inactive do
|
|
16
|
+
status { :inactive }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
factory :cm_stadium_x_v1_integration, parent: :cm_integration, class: SpreeCmCommissioner::Integrations::StadiumXV1 do
|
|
21
|
+
public_key { FFaker::Lorem.characters(20) }
|
|
22
|
+
private_key { FFaker::Lorem.characters(40) }
|
|
23
|
+
base_url { 'https://api.stadiumx.com' }
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
FactoryBot.define do
|
|
2
|
+
factory :cm_integration_mapping, class: SpreeCmCommissioner::IntegrationMapping do
|
|
3
|
+
external_id { SecureRandom.alphanumeric(10) }
|
|
4
|
+
association :integration, factory: :cm_integration
|
|
5
|
+
status { :active }
|
|
6
|
+
|
|
7
|
+
transient do
|
|
8
|
+
internal { create(:cm_product) }
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
internal_id { internal.id }
|
|
12
|
+
internal_type { internal.class.name }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -55,16 +55,17 @@ require 'byebug' if Rails.env.development? || Rails.env.test?
|
|
|
55
55
|
|
|
56
56
|
module SpreeCmCommissioner
|
|
57
57
|
class << self
|
|
58
|
-
# Allows overriding the default Redis connection
|
|
59
|
-
attr_writer :
|
|
58
|
+
# Allows overriding the default Redis connection pools with custom ones
|
|
59
|
+
attr_writer :inventory_redis_pool
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
# Inventory Redis pool for inventory management
|
|
62
|
+
def inventory_redis_pool
|
|
63
|
+
@inventory_redis_pool ||= default_redis_pool
|
|
63
64
|
end
|
|
64
65
|
|
|
65
|
-
# Resets
|
|
66
|
-
def
|
|
67
|
-
@
|
|
66
|
+
# Resets all Redis pools, useful for testing or reinitialization
|
|
67
|
+
def reset_redis_pools
|
|
68
|
+
@inventory_redis_pool = nil
|
|
68
69
|
end
|
|
69
70
|
|
|
70
71
|
private
|