spree_cm_commissioner 2.5.0.pre.pre2 → 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.
Files changed (100) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/test_and_build_gem.yml +1 -1
  3. data/.tool-versions +1 -1
  4. data/Gemfile.lock +1 -1
  5. data/app/controllers/spree/admin/integration_mappings_controller.rb +21 -0
  6. data/app/controllers/spree/admin/integration_sessions_controller.rb +21 -0
  7. data/app/controllers/spree/admin/integrations_controller.rb +83 -0
  8. data/app/controllers/spree/api/v2/storefront/event_matches_controller.rb +15 -0
  9. data/app/controllers/spree/api/v2/storefront/popular_route_places_controller.rb +1 -7
  10. data/app/controllers/spree/api/v2/storefront/route_places_controller.rb +9 -9
  11. data/app/errors/spree_cm_commissioner/integrations/external_client_error.rb +10 -0
  12. data/app/errors/spree_cm_commissioner/integrations/sync_error.rb +4 -0
  13. data/app/finders/spree_cm_commissioner/events/find_matches.rb +15 -0
  14. data/app/finders/spree_cm_commissioner/places/find_with_route.rb +10 -10
  15. data/app/finders/spree_cm_commissioner/routes/find_popular.rb +14 -10
  16. data/app/helpers/spree_cm_commissioner/external_integrations_helper.rb +58 -0
  17. data/app/interactors/spree_cm_commissioner/stock/inventory_item_resetter.rb +1 -1
  18. data/app/jobs/spree_cm_commissioner/integrations/base_job.rb +39 -0
  19. data/app/jobs/spree_cm_commissioner/integrations/polling_job.rb +53 -0
  20. data/app/jobs/spree_cm_commissioner/integrations/polling_scheduler_job.rb +30 -0
  21. data/app/jobs/spree_cm_commissioner/stock/inventory_items_adjuster_job.rb +3 -3
  22. data/app/jobs/spree_cm_commissioner/telegram_alerts/integration_sync_failure_job.rb +17 -0
  23. data/app/models/concerns/spree_cm_commissioner/integrations/integration_mappable.rb +61 -0
  24. data/app/models/concerns/spree_cm_commissioner/line_item_integration.rb +36 -0
  25. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +15 -3
  26. data/app/models/concerns/spree_cm_commissioner/order_integration.rb +33 -0
  27. data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +2 -0
  28. data/app/models/concerns/spree_cm_commissioner/store_metadata.rb +14 -2
  29. data/app/models/concerns/spree_cm_commissioner/tenant_preference.rb +0 -3
  30. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +1 -7
  31. data/app/models/spree_cm_commissioner/guest.rb +13 -0
  32. data/app/models/spree_cm_commissioner/integration.rb +29 -0
  33. data/app/models/spree_cm_commissioner/integration_mapping.rb +41 -0
  34. data/app/models/spree_cm_commissioner/integration_sync_session.rb +15 -0
  35. data/app/models/spree_cm_commissioner/integrations/stadium_x_v1.rb +37 -0
  36. data/app/models/spree_cm_commissioner/inventory_item.rb +5 -1
  37. data/app/models/spree_cm_commissioner/line_item_decorator.rb +13 -1
  38. data/app/models/spree_cm_commissioner/option_type_decorator.rb +8 -0
  39. data/app/models/spree_cm_commissioner/option_value_decorator.rb +34 -0
  40. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
  41. data/app/models/spree_cm_commissioner/product_decorator.rb +3 -2
  42. data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +2 -2
  43. data/app/models/spree_cm_commissioner/redis_stock/inventory_updater.rb +2 -2
  44. data/app/models/spree_cm_commissioner/taxon_decorator.rb +1 -0
  45. data/app/models/spree_cm_commissioner/taxonomy_decorator.rb +13 -1
  46. data/app/models/spree_cm_commissioner/variant_decorator.rb +7 -4
  47. data/app/models/spree_cm_commissioner/vendor_decorator.rb +4 -0
  48. data/app/overrides/spree/admin/shared/sub_menu/_integrations/external_integrations.html.erb.deface +6 -0
  49. data/app/presenters/spree/variants/{visable_options_presenter.rb → visible_options_presenter.rb} +2 -4
  50. data/app/request_schemas/spree_cm_commissioner/route_places_request_schema.rb +0 -5
  51. data/app/services/spree_cm_commissioner/integrations/base/sync_manager.rb +69 -0
  52. data/app/services/spree_cm_commissioner/integrations/base/sync_result.rb +183 -0
  53. data/app/services/spree_cm_commissioner/integrations/polling.rb +70 -0
  54. data/app/services/spree_cm_commissioner/integrations/polling_scheduler.rb +79 -0
  55. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/external_client/client.rb +152 -0
  56. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/inventory/unstock_inventory.rb +83 -0
  57. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_matches.rb +113 -0
  58. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/polling/sync_zones.rb +215 -0
  59. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/base.rb +20 -0
  60. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/league.rb +19 -0
  61. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/match.rb +95 -0
  62. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket.rb +81 -0
  63. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/ticket_image.rb +19 -0
  64. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/resources/zone.rb +90 -0
  65. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_manager.rb +35 -0
  66. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/full_sync_strategy.rb +38 -0
  67. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/incremental_sync_strategy.rb +44 -0
  68. data/app/services/spree_cm_commissioner/integrations/stadium_x_v1/sync_strategies/webhook_sync_strategy.rb +16 -0
  69. data/app/services/spree_cm_commissioner/telegram_alerts/integration_sync_failure.rb +49 -0
  70. data/app/views/spree/admin/integration_mappings/_integration_mappings.html.erb +107 -0
  71. data/app/views/spree/admin/integration_mappings/index.html.erb +33 -0
  72. data/app/views/spree/admin/integration_sessions/_integration_sync_sessions.html.erb +116 -0
  73. data/app/views/spree/admin/integration_sessions/index.html.erb +42 -0
  74. data/app/views/spree/admin/integrations/_form.html.erb +104 -0
  75. data/app/views/spree/admin/integrations/_stadium_x_v1_fields.html.erb +29 -0
  76. data/app/views/spree/admin/integrations/edit.html.erb +45 -0
  77. data/app/views/spree/admin/integrations/index.html.erb +75 -0
  78. data/app/views/spree/admin/integrations/new.html.erb +25 -0
  79. data/app/views/spree/admin/tenants/_form.html.erb +0 -18
  80. data/app/views/spree/order_mailer/confirm_email.html.erb +1 -1
  81. data/app/views/spree_cm_commissioner/layouts/order_mailer.html.erb +1 -1
  82. data/app/views/spree_cm_commissioner/order_mailer/tenant/_footer.html.erb +6 -13
  83. data/app/views/spree_cm_commissioner/order_mailer/tenant/_support_contact.html.erb +24 -23
  84. data/config/locales/en.yml +8 -0
  85. data/config/locales/km.yml +8 -0
  86. data/config/routes.rb +8 -0
  87. data/db/migrate/20251017094845_create_cm_integrations.rb +22 -0
  88. data/db/migrate/20251017101555_create_cm_integration_sync_sessions.rb +68 -0
  89. data/db/migrate/20251017101605_create_cm_integration_mappings.rb +52 -0
  90. data/lib/cm_app_logger.rb +1 -0
  91. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +25 -0
  92. data/lib/spree_cm_commissioner/test_helper/factories/integration_mapping_factory.rb +14 -0
  93. data/lib/spree_cm_commissioner/test_helper/factories/integration_sync_session_factory.rb +7 -0
  94. data/lib/spree_cm_commissioner/version.rb +1 -1
  95. data/lib/spree_cm_commissioner.rb +8 -7
  96. metadata +56 -6
  97. data/app/request_schemas/spree_cm_commissioner/popular_route_places_request_schema.rb +0 -12
  98. data/app/views/spree/admin/tenants/form/_footer.html.erb +0 -31
  99. data/app/views/spree/admin/tenants/form/_social.html.erb +0 -31
  100. data/db/migrate/20251209022924_add_contact_fields_to_cm_tenants.rb +0 -9
@@ -0,0 +1,90 @@
1
+ class SpreeCmCommissioner::Integrations::StadiumXV1
2
+ module Resources
3
+ class Zone < Base
4
+ attr_accessor :_id,
5
+ :title,
6
+ :color_brand,
7
+ :total_seats,
8
+ :online_price,
9
+ :walkin_price,
10
+ :match_id,
11
+ :club_id,
12
+ :is_active,
13
+ :created_by,
14
+ :updated_by,
15
+ :created_at,
16
+ :updated_at,
17
+ :end_datetime,
18
+ :is_earlybird,
19
+ :start_datetime,
20
+ :available_seats,
21
+ :ticket_image
22
+
23
+ def initialize(attributes = {})
24
+ super(attributes)
25
+
26
+ @total_seats = attributes['total_seats'].to_i
27
+ @available_seats = attributes['available_seats'].to_i
28
+ @online_price = attributes['online_price'].to_f
29
+ @walkin_price = attributes['walkin_price'].to_f
30
+ @is_active = attributes['is_active'] != false
31
+ @color_brand = valid_hex_color?(attributes['color_brand']) ? attributes['color_brand'].upcase : nil
32
+
33
+ @created_at = Time.zone.at(attributes['created_at'] / 1000) if attributes['created_at'].present?
34
+ @updated_at = Time.zone.at(attributes['updated_at'] / 1000) if attributes['updated_at'].present?
35
+
36
+ @start_datetime = Time.zone.parse(attributes['start_datetime']) if attributes['start_datetime'].present?
37
+ @end_datetime = Time.zone.parse(attributes['end_datetime']) if attributes['end_datetime'].present?
38
+ @ticket_image = TicketImage.new(attributes['ticket_image']) if attributes['ticket_image'].is_a?(Hash)
39
+ end
40
+
41
+ # Manually set the match_id on the zone object.
42
+ # This is needed because the API does not return match_id but it's required for ticket creation.
43
+ # This is a temporary workaround until the API returns the match_id.
44
+ def set_match_id(match_id) # rubocop:disable Naming/AccessorMethodName
45
+ @match_id = match_id
46
+ end
47
+
48
+ def to_hash
49
+ hash = {
50
+ '_id' => _id,
51
+ 'title' => title,
52
+ 'color_brand' => color_brand,
53
+ 'total_seats' => total_seats,
54
+ 'online_price' => online_price,
55
+ 'walkin_price' => walkin_price,
56
+ 'match_id' => match_id,
57
+ 'club_id' => club_id,
58
+ 'is_active' => is_active,
59
+ 'created_by' => created_by,
60
+ 'updated_by' => updated_by,
61
+ 'created_at' => created_at&.iso8601,
62
+ 'updated_at' => updated_at&.iso8601,
63
+ 'end_datetime' => end_datetime&.iso8601,
64
+ 'is_earlybird' => is_earlybird,
65
+ 'start_datetime' => start_datetime&.iso8601,
66
+ 'available_seats' => available_seats,
67
+ 'ticket_image' => ticket_image&.to_h
68
+ }.compact
69
+
70
+ hash['ticket_image'] = hash['ticket_image'].transform_keys(&:to_s) if hash['ticket_image']
71
+
72
+ hash
73
+ end
74
+
75
+ alias to_h to_hash
76
+
77
+ private
78
+
79
+ # Validates if a color value is a valid hex color code.
80
+ # Accepts formats: #RRGGBB, #RRGGBBAA, RRGGBB, RRGGBBAA (with or without # prefix)
81
+ # Returns true only if the value is a valid hex color, false otherwise.
82
+ def valid_hex_color?(value)
83
+ return false if value.blank?
84
+
85
+ hex_pattern = /\A#?([0-9a-fA-F]{6}|[0-9a-fA-F]{8})\z/
86
+ hex_pattern.match?(value.to_s)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,35 @@
1
+ class SpreeCmCommissioner::Integrations::StadiumXV1
2
+ class SyncManager < ::SpreeCmCommissioner::Integrations::Base::SyncManager
3
+ # override
4
+ def sync_full!
5
+ run_execution(:full) do
6
+ SyncStrategies::FullSyncStrategy.new(
7
+ client: @client,
8
+ integration: @integration
9
+ ).call
10
+ end
11
+ end
12
+
13
+ # override
14
+ def sync_incremental!
15
+ run_execution(:incremental) do
16
+ SyncStrategies::IncrementalSyncStrategy.new(
17
+ client: @client,
18
+ integration: @integration
19
+ ).call
20
+ end
21
+ end
22
+
23
+ # override
24
+ def sync_webhook!(event_type:, event_data:)
25
+ run_execution(:webhook) do
26
+ SyncStrategies::WebhookSyncStrategy.new(
27
+ client: @client,
28
+ integration: @integration,
29
+ event_type: event_type,
30
+ event_data: event_data
31
+ ).call
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,38 @@
1
+ class SpreeCmCommissioner::Integrations::StadiumXV1
2
+ module SyncStrategies
3
+ # Full sync strategy - syncs all matches from external system.
4
+ #
5
+ # Runs on a schedule (e.g., daily) to fetch and update all matches from Stadium X V1 API.
6
+ # After full sync, IncrementalSyncStrategy will sync zone data one match at a time.
7
+ #
8
+ # Example flow:
9
+ # 1. Full sync runs at 2:00 AM
10
+ # - Fetches all matches from Stadium X V1
11
+ # - Creates/updates match records
12
+ # - Creates integration mappings
13
+ # 2. Incremental sync runs every 10 seconds
14
+ # - Syncs zone data for one match per interval
15
+ # - Keeps data fresh without overwhelming the system
16
+ class FullSyncStrategy
17
+ def initialize(client:, integration:)
18
+ @client = client
19
+ @integration = integration
20
+ @sync_result = ::SpreeCmCommissioner::Integrations::Base::SyncResult.new
21
+ end
22
+
23
+ def call
24
+ begin
25
+ Polling::SyncMatches.new(
26
+ client: @client,
27
+ integration: @integration,
28
+ sync_result: @sync_result
29
+ ).call
30
+ rescue StandardError => e
31
+ @sync_result.record_error(e)
32
+ end
33
+
34
+ @sync_result
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,44 @@
1
+ class SpreeCmCommissioner::Integrations::StadiumXV1
2
+ module SyncStrategies
3
+ # Incremental sync strategy - syncs zones one match at a time.
4
+ #
5
+ # Runs every 10 seconds to sync zone data incrementally. Instead of syncing all matches
6
+ # at once (which would be slow), we sync one match per interval in round-robin order.
7
+ #
8
+ # Example with 3 matches:
9
+ # 10:00:00 - Sync zones for Match #1 (oldest last_synced_at)
10
+ # 10:00:10 - Sync zones for Match #2
11
+ # 10:00:20 - Sync zones for Match #3
12
+ # 10:00:30 - Sync zones for Match #1 again (cycle repeats)
13
+ #
14
+ # This ensures all matches get synced regularly without overwhelming the system.
15
+ class IncrementalSyncStrategy
16
+ def initialize(client:, integration:)
17
+ @client = client
18
+ @integration = integration
19
+ @sync_result = ::SpreeCmCommissioner::Integrations::Base::SyncResult.new
20
+ end
21
+
22
+ def call
23
+ begin
24
+ oldest_sync_mapping = Spree::Taxon.find_oldest_active_mapping(integration_id: @integration.id)
25
+ return @sync_result if oldest_sync_mapping.nil?
26
+
27
+ @sync_result.increment_metric(:match, :synced)
28
+
29
+ Polling::SyncZones.new(
30
+ integration: @integration,
31
+ client: @integration.client,
32
+ sync_result: @sync_result
33
+ ).call(external_match_id: oldest_sync_mapping.external_id)
34
+
35
+ oldest_sync_mapping.update!(last_synced_at: Time.current)
36
+ rescue StandardError => e
37
+ @sync_result.record_error(e)
38
+ end
39
+
40
+ @sync_result
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ class SpreeCmCommissioner::Integrations::StadiumXV1
2
+ module SyncStrategies
3
+ class WebhookSyncStrategy
4
+ def initialize(client:, integration:, event_type:, event_data:)
5
+ @client = client
6
+ @integration = integration
7
+ @event_type = event_type
8
+ @event_data = event_data
9
+ end
10
+
11
+ def call
12
+ # TODO: Implement webhook sync logic
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,49 @@
1
+ module SpreeCmCommissioner
2
+ module TelegramAlerts
3
+ class IntegrationSyncFailure
4
+ prepend ::Spree::ServiceModule::Base
5
+
6
+ # Send Telegram alert for integration sync failure
7
+ # @param error_message [String] The error message
8
+ # @param data [Hash] Additional context data (e.g., sync_type, match_id, event_type)
9
+ def call(error_message:, data: {})
10
+ chat_id = ENV.fetch('EXCEPTION_NOTIFIER_TELEGRAM_CHANNEL_ID', nil)
11
+ return failure(nil, 'Telegram chat ID not configured') if chat_id.blank?
12
+
13
+ telegram_client = ::Telegram.bots[:exception_notifier]
14
+ return failure(nil, 'Telegram bot not configured') if telegram_client.blank?
15
+
16
+ telegram_client.send_message(
17
+ chat_id: chat_id,
18
+ parse_mode: 'HTML',
19
+ text: format_message(error_message, data)
20
+ )
21
+
22
+ success(message: 'Alert sent successfully')
23
+ rescue StandardError => e
24
+ failure(nil, "Failed to send Telegram alert: #{e.message}")
25
+ end
26
+
27
+ private
28
+
29
+ def format_message(error_message, data)
30
+ text = []
31
+
32
+ text << '🚨 <b>Integration Sync Failed</b>'
33
+
34
+ # Render all data fields dynamically
35
+ data.each do |key, value|
36
+ next if value.blank?
37
+
38
+ formatted_key = key.to_s.humanize
39
+ text << "<b>#{formatted_key}:</b> <code>#{value}</code>"
40
+ end
41
+
42
+ text << "<b>Error:</b> <code>#{error_message}</code>"
43
+ text << "<b>Time:</b> <code>#{Time.current.iso8601}</code>"
44
+
45
+ text.compact.join("\n")
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,107 @@
1
+ <div class="table-responsive">
2
+ <table class="table table-sm" id="listing_integration_mappings" data-hook>
3
+ <thead class="text-muted">
4
+ <tr>
5
+ <th><%= Spree.t(:id) %></th>
6
+ <th><%= Spree.t(:external_id) %></th>
7
+ <th><%= Spree.t(:internal_type) %></th>
8
+ <th><%= Spree.t(:internal_id) %></th>
9
+ <th><%= Spree.t(:status) %></th>
10
+ <th><%= Spree.t(:last_synced_at) %></th>
11
+ <th><%= Spree.t(:created_at) %></th>
12
+ <th><%= Spree.t(:updated_at) %></th>
13
+ <th></th>
14
+ </tr>
15
+ </thead>
16
+
17
+ <tbody>
18
+ <% mappings = defined?(@mappings) ? @mappings : integration.integration_mappings.order(updated_at: :desc).limit(25) %>
19
+ <% mappings.each do |mapping| %>
20
+ <tr id="integration_mapping_<%= mapping.id %>">
21
+ <td><%= mapping.id %></td>
22
+ <td><%= mapping.external_id %></td>
23
+ <td><%= mapping.internal_type %></td>
24
+ <td>
25
+ <%= integration_mapping_internal_link(mapping) %>
26
+ </td>
27
+ <td><%= mapping.status %></td>
28
+ <td><%= mapping.last_synced_at %></td>
29
+ <td><%= mapping.created_at %></td>
30
+ <td><%= mapping.updated_at %></td>
31
+
32
+ <td class="actions">
33
+ <% if mapping.external_payload.present? %>
34
+ <%= link_to_with_icon 'activity.svg', Spree.t("admin.external_integrations.view_result"), "#mapping_payload_modal_#{mapping.id}",
35
+ class: 'btn btn-light btn-sm',
36
+ no_text: true,
37
+ data: { toggle: 'modal', target: "#mapping_payload_modal_#{mapping.id}" },
38
+ title: Spree.t("admin.external_integrations.external_payload") %>
39
+ <% end %>
40
+ </td>
41
+ </tr>
42
+ <% end %>
43
+ </tbody>
44
+ </table>
45
+
46
+ <% if mappings.empty? %>
47
+ <small class="form-text text-muted"><%= Spree.t("admin.external_integrations.no_results") %></small>
48
+ <% end %>
49
+
50
+ <% if defined?(@mappings) && @mappings.respond_to?(:total_pages) %>
51
+ <div class="mt-3">
52
+ <%= paginate @mappings, theme: 'admin-twitter-bootstrap-4' %>
53
+ </div>
54
+ <% end %>
55
+ </div>
56
+
57
+ <% mappings.each do |mapping| %>
58
+ <% if mapping.external_payload.present? %>
59
+ <div
60
+ class="modal fade"
61
+ id="mapping_payload_modal_<%= mapping.id %>"
62
+ tabindex="-1"
63
+ role="dialog"
64
+ aria-labelledby="mapping_payload_modal_label_<%= mapping.id %>"
65
+ aria-hidden="true"
66
+ >
67
+ <div class="modal-dialog modal-lg" role="document">
68
+ <div class="modal-content">
69
+ <div class="modal-header">
70
+ <h5
71
+ class="modal-title"
72
+ id="mapping_payload_modal_label_<%= mapping.id %>"
73
+ >
74
+ <%= Spree.t("admin.external_integrations.external_payload") %> - Mapping #<%= mapping.id %>
75
+ </h5>
76
+
77
+ <button
78
+ type="button"
79
+ class="close"
80
+ data-dismiss="modal"
81
+ aria-label="Close"
82
+ >
83
+ <span aria-hidden="true">&times;</span>
84
+ </button>
85
+ </div>
86
+
87
+ <div class="modal-body">
88
+ <pre
89
+ class="small mb-0 p-2 bg-light border rounded"
90
+ style="max-height: 500px; overflow-y: auto;"
91
+ ><code><%= JSON.pretty_generate(mapping.external_payload) %></code></pre>
92
+ </div>
93
+
94
+ <div class="modal-footer">
95
+ <button
96
+ type="button"
97
+ class="btn btn-secondary"
98
+ data-dismiss="modal"
99
+ >
100
+ <%= Spree.t(:close) %>
101
+ </button>
102
+ </div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+ <% end %>
107
+ <% end %>
@@ -0,0 +1,33 @@
1
+ <% content_for :page_title do %>
2
+ <%= link_to Spree.t('external_integrations'), spree.admin_integrations_url %> / <%= @integration.name %>
3
+ <% end %>
4
+
5
+ <% content_for :page_tabs do %>
6
+ <li class="nav-item">
7
+ <%= link_to Spree.t(:edit),
8
+ edit_admin_integration_path(@integration),
9
+ class: "nav-link" %>
10
+ </li>
11
+
12
+ <li class="nav-item">
13
+ <%= link_to Spree.t(:integration_sync_sessions),
14
+ admin_integration_sessions_path(@integration),
15
+ class: "nav-link" %>
16
+ </li>
17
+
18
+ <li class="nav-item">
19
+ <%= link_to Spree.t(:integration_mappings),
20
+ admin_integration_mappings_path(@integration),
21
+ class: "nav-link active" %>
22
+ </li>
23
+ <% end %>
24
+
25
+ <div data-hook="admin_integrations_mappings" class="card mb-3">
26
+ <div class="card-header">
27
+ <h5 class="card-title mb-0 h6"><%= Spree.t(:integration_mappings) %></h5>
28
+ </div>
29
+
30
+ <div class="card-body">
31
+ <%= render partial: 'integration_mappings', locals: { integration: @integration } %>
32
+ </div>
33
+ </div>
@@ -0,0 +1,116 @@
1
+ <div class="table-responsive">
2
+ <table
3
+ class="table table-sm"
4
+ id="listing_integration_sync_sessions"
5
+ data-hook
6
+ >
7
+ <thead class="text-muted">
8
+ <tr>
9
+ <th><%= Spree.t(:id) %></th>
10
+ <th><%= Spree.t(:sync_type) %></th>
11
+ <th><%= Spree.t(:status) %></th>
12
+ <th><%= Spree.t(:started_at) %></th>
13
+ <th><%= Spree.t(:ended_at) %></th>
14
+ <th><%= Spree.t(:duration) %></th>
15
+ <th></th>
16
+ </tr>
17
+ </thead>
18
+
19
+ <tbody>
20
+ <% sync_sessions = defined?(@sync_sessions) ? @sync_sessions : integration.integration_sync_sessions.order(created_at: :desc).limit(25) %>
21
+ <% sync_sessions.each do |sess| %>
22
+ <tr id="integration_sync_session_<%= sess.id %>">
23
+ <td><%= sess.id %></td>
24
+ <td><%= sess.sync_type %></td>
25
+ <td><%= sess.status %></td>
26
+ <td><%= sess.started_at %></td>
27
+ <td><%= sess.ended_at %></td>
28
+ <td>
29
+ <% if sess.started_at && sess.ended_at %>
30
+ <% duration = (sess.ended_at - sess.started_at).to_i %>
31
+
32
+ <% if duration < 60 %>
33
+ <%= "#{duration} sec" %>
34
+ <% else %>
35
+ <%= "#{duration / 60} min" %>
36
+ <% end %>
37
+ <% else %>
38
+ N/A
39
+ <% end %>
40
+ </td>
41
+ <td class="actions">
42
+ <% if sess.sync_result.present? %>
43
+ <%= link_to_with_icon 'activity.svg', Spree.t("admin.external_integrations.view_result"), "#session_result_modal_#{sess.id}",
44
+ class: 'btn btn-light btn-sm',
45
+ no_text: true,
46
+ data: { toggle: 'modal', target: "#session_result_modal_#{sess.id}" },
47
+ title: Spree.t("admin.external_integrations.sync_result") %>
48
+ <% end %>
49
+ </td>
50
+ </tr>
51
+ <% end %>
52
+ </tbody>
53
+ </table>
54
+
55
+ <% if sync_sessions.empty? %>
56
+ <small class="form-text text-muted"><%= Spree.t("admin.external_integrations.no_results") %></small>
57
+ <% end %>
58
+
59
+ <% if defined?(@sync_sessions) && @sync_sessions.respond_to?(:total_pages) %>
60
+ <div class="mt-3">
61
+ <%= paginate @sync_sessions, theme: 'admin-twitter-bootstrap-4' %>
62
+ </div>
63
+ <% end %>
64
+ </div>
65
+
66
+ <% sync_sessions.each do |sess| %>
67
+ <% if sess.sync_result.present? %>
68
+ <div
69
+ class="modal fade"
70
+ id="session_result_modal_<%= sess.id %>"
71
+ tabindex="-1"
72
+ role="dialog"
73
+ aria-labelledby="session_result_modal_label_<%= sess.id %>"
74
+ aria-hidden="true"
75
+ >
76
+ <div class="modal-dialog modal-lg" role="document">
77
+ <div class="modal-content">
78
+ <div class="modal-header">
79
+ <h5
80
+ class="modal-title"
81
+ id="session_result_modal_label_<%= sess.id %>"
82
+ >
83
+ <%= Spree.t("admin.external_integrations.sync_result") %> - Session #<%= sess.id %>
84
+ </h5>
85
+
86
+ <button
87
+ type="button"
88
+ class="close"
89
+ data-dismiss="modal"
90
+ aria-label="Close"
91
+ >
92
+ <span aria-hidden="true">&times;</span>
93
+ </button>
94
+ </div>
95
+
96
+ <div class="modal-body">
97
+ <pre
98
+ class="small mb-0 p-2 bg-light border rounded"
99
+ style="max-height: 500px; overflow-y: auto;"
100
+ ><code><%= JSON.pretty_generate(sess.sync_result) %></code></pre>
101
+ </div>
102
+
103
+ <div class="modal-footer">
104
+ <button
105
+ type="button"
106
+ class="btn btn-secondary"
107
+ data-dismiss="modal"
108
+ >
109
+ <%= Spree.t(:close) %>
110
+ </button>
111
+ </div>
112
+ </div>
113
+ </div>
114
+ </div>
115
+ <% end %>
116
+ <% end %>
@@ -0,0 +1,42 @@
1
+ <% content_for :page_title do %>
2
+ <%= link_to Spree.t('external_integrations'), spree.admin_integrations_url %> / <%= @integration.name %>
3
+ <% end %>
4
+
5
+ <% content_for :page_tabs do %>
6
+ <li class="nav-item">
7
+ <%= link_to Spree.t(:edit),
8
+ edit_admin_integration_path(@integration),
9
+ class: "nav-link" %>
10
+ </li>
11
+
12
+ <li class="nav-item">
13
+ <%= link_to Spree.t(:integration_sync_sessions),
14
+ admin_integration_sessions_path(@integration),
15
+ class: "nav-link active" %>
16
+ </li>
17
+
18
+ <li class="nav-item">
19
+ <%= link_to Spree.t(:integration_mappings),
20
+ admin_integration_mappings_path(@integration),
21
+ class: "nav-link" %>
22
+ </li>
23
+ <% end %>
24
+
25
+ <div data-hook="admin_integrations_sessions" class="card mb-3">
26
+ <div class="card-header d-flex justify-content-between align-items-center">
27
+ <h5 class="card-title mb-0 h6">
28
+ <%= Spree.t(:integration_sync_sessions) %>
29
+ </h5>
30
+
31
+ <div class="card-header-actions">
32
+ <%= button_to Spree.t(:enqueue_full_sync),
33
+ enqueue_polling_admin_integration_path(@integration, sync_type: :full),
34
+ method: :post,
35
+ class: 'btn btn-primary btn-sm' %>
36
+ </div>
37
+ </div>
38
+
39
+ <div class="card-body">
40
+ <%= render partial: 'integration_sync_sessions', locals: { integration: @integration } %>
41
+ </div>
42
+ </div>
@@ -0,0 +1,104 @@
1
+ <div data-hook="admin_external_integrations_form_fields" class="row">
2
+ <div class="col-6">
3
+ <%= f.field_container :type do %>
4
+ <%= f.label :type, raw(I18n.t('spree.admin.external_integrations.integration_type', default: 'Integration type') + required_span_tag) %>
5
+
6
+ <% if f.object.persisted? %>
7
+ <%= f.text_field :type, value: f.object.type, class: 'form-control', disabled: true, readonly: true %>
8
+ <%= f.hidden_field :type, value: f.object.type %>
9
+ <% else %>
10
+ <%= f.select :type,
11
+ options_for_select(@integration_type_options, f.object.type),
12
+ { include_blank: Spree.t(:choose_an_option) },
13
+ class: 'form-control select2',
14
+ required: true %>
15
+ <% end %>
16
+
17
+ <%= f.error_message_on :type %>
18
+ <% end %>
19
+ </div>
20
+
21
+ <div class="col-6">
22
+ <%= f.field_container :name do %>
23
+ <%= f.label :name, raw(Spree.t(:name) + required_span_tag) %>
24
+ <%= f.text_field :name, required: true, class: 'form-control', placeholder: Spree.t(:name) %>
25
+ <%= f.error_message_on :name %>
26
+ <% end %>
27
+ </div>
28
+
29
+ <div class="col-6">
30
+ <%= f.field_container :vendor_id do %>
31
+ <%= f.label :vendor_id, raw(I18n.t('spree.admin.external_integrations.vendor', default: 'Vendor') + required_span_tag) %>
32
+
33
+ <%= f.select :vendor_id,
34
+ options_from_collection_for_select(@vendors, :id, :name, f.object.vendor_id),
35
+ { include_blank: Spree.t(:choose_an_option) },
36
+ class: 'form-control select2',
37
+ required: true %>
38
+
39
+ <%= f.error_message_on :vendor_id %>
40
+ <% end %>
41
+ </div>
42
+
43
+ <div class="col-6">
44
+ <%= f.field_container :tenant_id do %>
45
+ <%= f.label :tenant_id, I18n.t('spree.admin.external_integrations.tenant', default: 'Tenant') %>
46
+
47
+ <%= f.select :tenant_id,
48
+ options_from_collection_for_select(@tenants, :id, :name, f.object.tenant_id),
49
+ { include_blank: Spree.t(:choose_an_option), prompt: Spree.t(:optional) },
50
+ class: 'form-control select2',
51
+ data: { allow_clear: true, placeholder: Spree.t(:optional) } %>
52
+
53
+ <%= f.error_message_on :tenant_id %>
54
+ <% end %>
55
+ </div>
56
+
57
+ <div class="col-6">
58
+ <%= f.field_container :status do %>
59
+ <%= f.label :status, raw(Spree.t(:status) + required_span_tag) %>
60
+
61
+ <%= f.select :status,
62
+ options_for_select(@status_options, f.object.status),
63
+ {},
64
+ class: 'form-control select2',
65
+ required: true %>
66
+
67
+ <%= f.error_message_on :status %>
68
+ <% end %>
69
+ </div>
70
+
71
+ <div class="col-6">
72
+ <%= f.field_container :conflict_strategy do %>
73
+ <%= f.label :conflict_strategy, raw(I18n.t('spree.admin.external_integrations.conflict_strategy', default: 'Conflict strategy') + required_span_tag) %>
74
+
75
+ <%= f.select :conflict_strategy,
76
+ options_for_select(@conflict_strategy_options, f.object.conflict_strategy),
77
+ {},
78
+ class: 'form-control select2',
79
+ required: true %>
80
+
81
+ <%= f.error_message_on :conflict_strategy %>
82
+ <% end %>
83
+ </div>
84
+
85
+ <div class="col-6">
86
+ <%= f.field_container :incremental_sync_interval_seconds do %>
87
+ <%= f.label :incremental_sync_interval_seconds, raw(I18n.t('spree.admin.external_integrations.incremental_sync_interval_seconds', default: 'Incremental sync interval (seconds)') + required_span_tag) %>
88
+ <%= f.number_field :incremental_sync_interval_seconds, class: 'form-control', min: 1, max: 3600, required: true %>
89
+ <%= f.error_message_on :incremental_sync_interval_seconds %>
90
+ <% end %>
91
+ </div>
92
+
93
+ <div class="col-6">
94
+ <%= f.field_container :full_sync_interval_hours do %>
95
+ <%= f.label :full_sync_interval_hours, raw(I18n.t('spree.admin.external_integrations.full_sync_interval_hours', default: 'Full sync interval (hours)') + required_span_tag) %>
96
+ <%= f.number_field :full_sync_interval_hours, class: 'form-control', min: 1, max: 168, required: true %>
97
+ <%= f.error_message_on :full_sync_interval_hours %>
98
+ <% end %>
99
+ </div>
100
+
101
+ <% if f.object.type == 'SpreeCmCommissioner::Integrations::StadiumXV1' || (f.object.new_record? && params[:type] == 'SpreeCmCommissioner::Integrations::StadiumXV1') %>
102
+ <%= render partial: 'stadium_x_v1_fields', locals: { f: f } %>
103
+ <% end %>
104
+ </div>