spree_cm_commissioner 2.13.7 → 2.14.0

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 (208) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/assets/javascripts/spree_cm_commissioner/backend.js +0 -1
  4. data/app/assets/stylesheets/spree_cm_commissioner/backend/calendar.scss +0 -39
  5. data/app/controllers/concerns/spree/admin/rates_availability_cells.rb +16 -0
  6. data/app/controllers/concerns/spree/api/v2/product_list_includes_decorator.rb +1 -0
  7. data/app/controllers/spree/admin/guests_controller.rb +2 -1
  8. data/app/controllers/spree/admin/integrations_controller.rb +16 -7
  9. data/app/controllers/spree/admin/inventory_items_controller.rb +33 -62
  10. data/app/controllers/spree/admin/product_option_types_controller.rb +88 -0
  11. data/app/controllers/spree/admin/rate_plans_controller.rb +125 -0
  12. data/app/controllers/spree/admin/rates_availabilities_controller.rb +142 -0
  13. data/app/controllers/spree/admin/stock_managements_controller.rb +1 -53
  14. data/app/controllers/spree/api/switch_cm/base_controller.rb +109 -0
  15. data/app/controllers/spree/api/switch_cm/tokens_controller.rb +68 -0
  16. data/app/controllers/spree/api/switch_cm/xml/availabilities_controller.rb +39 -0
  17. data/app/controllers/spree/api/switch_cm/xml/base_controller.rb +37 -0
  18. data/app/controllers/spree/api/switch_cm/xml/reservation_summaries_controller.rb +28 -0
  19. data/app/controllers/spree/api/switch_cm/xml/reservations_controller.rb +57 -0
  20. data/app/controllers/spree/api/switch_cm/xml/room_rate_availabilities_controller.rb +56 -0
  21. data/app/controllers/spree/api/switch_cm/xml/room_rates_controller.rb +45 -0
  22. data/app/controllers/spree/api/v2/operator/guests_controller.rb +1 -1
  23. data/app/controllers/spree/api/v2/storefront/accommodations/variants_controller.rb +31 -5
  24. data/app/controllers/spree/api/v2/storefront/inventory_items_controller.rb +2 -2
  25. data/app/controllers/spree/api/v2/tenant/intercity_taxi/draft_orders_controller.rb +0 -1
  26. data/app/factory/spree_cm_commissioner/order_telegram_message_factory.rb +1 -1
  27. data/app/finders/spree_cm_commissioner/accommodations/find.rb +10 -8
  28. data/app/finders/spree_cm_commissioner/accommodations/find_variant.rb +5 -11
  29. data/app/finders/spree_cm_commissioner/rates_availabilities/find_bookings.rb +116 -0
  30. data/app/helpers/spree_cm_commissioner/admin/inventory_items_helper.rb +36 -0
  31. data/app/helpers/spree_cm_commissioner/admin/rates_availabilities_helper.rb +247 -0
  32. data/app/interactors/spree_cm_commissioner/intercity_taxi_order_creator.rb +0 -1
  33. data/app/jobs/spree_cm_commissioner/option_type_variants_public_metadata_updater_job.rb +1 -0
  34. data/app/jobs/spree_cm_commissioner/option_value_variants_public_metadata_updater_job.rb +1 -0
  35. data/app/jobs/spree_cm_commissioner/product_variants_public_metadata_updater_job.rb +12 -0
  36. data/app/models/concerns/spree_cm_commissioner/kyc_bitwise.rb +4 -0
  37. data/app/models/concerns/spree_cm_commissioner/line_item_durationable.rb +1 -10
  38. data/app/models/concerns/spree_cm_commissioner/line_item_guests_concern.rb +33 -3
  39. data/app/models/concerns/spree_cm_commissioner/line_item_rate_plannable.rb +55 -0
  40. data/app/models/concerns/spree_cm_commissioner/line_item_transitable.rb +0 -16
  41. data/app/models/concerns/spree_cm_commissioner/option_type_attr_type.rb +22 -4
  42. data/app/models/concerns/spree_cm_commissioner/rate_plan_options_concern.rb +62 -0
  43. data/app/models/concerns/spree_cm_commissioner/variant_options_concern.rb +12 -0
  44. data/app/models/concerns/spree_cm_commissioner/variant_rate_plannable.rb +81 -0
  45. data/app/models/spree_cm_commissioner/address_decorator.rb +2 -0
  46. data/app/models/spree_cm_commissioner/configuration.rb +0 -6
  47. data/app/models/spree_cm_commissioner/exports/import_order_guests_csv.rb +2 -1
  48. data/app/models/spree_cm_commissioner/exports/operator_guest_json_gzip.rb +8 -2
  49. data/app/models/spree_cm_commissioner/guest.rb +1 -1
  50. data/app/models/spree_cm_commissioner/integration.rb +15 -0
  51. data/app/models/spree_cm_commissioner/integrations/switch_cm.rb +96 -0
  52. data/app/models/spree_cm_commissioner/inventory_item.rb +32 -3
  53. data/app/models/spree_cm_commissioner/line_item_decorator.rb +7 -7
  54. data/app/models/spree_cm_commissioner/maintenance_tasks/cache_invalidation.rb +20 -4
  55. data/app/models/spree_cm_commissioner/maintenance_tasks/event.rb +3 -2
  56. data/app/models/spree_cm_commissioner/option_type_decorator.rb +2 -1
  57. data/app/models/spree_cm_commissioner/option_value_decorator.rb +3 -0
  58. data/app/models/spree_cm_commissioner/option_value_rate_plan.rb +12 -0
  59. data/app/models/spree_cm_commissioner/order_decorator.rb +1 -0
  60. data/app/models/spree_cm_commissioner/price_decorator.rb +13 -0
  61. data/app/models/spree_cm_commissioner/product_decorator.rb +18 -2
  62. data/app/models/spree_cm_commissioner/product_option_type_decorator.rb +42 -0
  63. data/app/models/spree_cm_commissioner/rate_plan.rb +139 -0
  64. data/app/models/spree_cm_commissioner/rate_plan_options.rb +36 -0
  65. data/app/models/spree_cm_commissioner/rate_restriction.rb +45 -0
  66. data/app/models/spree_cm_commissioner/taxon_decorator.rb +5 -2
  67. data/app/models/spree_cm_commissioner/variant_decorator.rb +36 -5
  68. data/app/models/spree_cm_commissioner/variant_options.rb +3 -2
  69. data/app/models/spree_cm_commissioner/vendor_decorator.rb +1 -0
  70. data/app/overrides/spree/admin/option_types/_form/checkbox.html.erb.deface +0 -8
  71. data/app/overrides/spree/admin/option_types/_form/name.html.erb.deface +1 -1
  72. data/app/overrides/spree/admin/products/_filters/vendor_filter.html.erb.deface +6 -0
  73. data/app/overrides/spree/admin/shared/_product_tabs/rates_availabilities.html.erb.deface +8 -0
  74. data/app/overrides/spree/admin/shared/_product_tabs/stock_managements.html.erb.deface +1 -1
  75. data/app/overrides/spree/admin/variants/index/discontinued_badge.html.erb.deface +13 -0
  76. data/app/overrides/spree/admin/variants/index/option_types_empty_state_link.html.erb.deface +3 -0
  77. data/app/overrides/spree/admin/variants/index/option_types_summary.html.erb.deface +24 -0
  78. data/app/presenters/spree/variants/visible_options_presenter.rb +34 -1
  79. data/app/queries/spree_cm_commissioner/multi_leg_trips_query.rb +40 -22
  80. data/app/queries/spree_cm_commissioner/single_leg_trips_query.rb +28 -14
  81. data/app/request_schemas/spree_cm_commissioner/intercity_taxi_draft_order_update_schema.rb +0 -1
  82. data/app/serializers/spree/v2/storefront/line_item_serializer_decorator.rb +2 -0
  83. data/app/serializers/spree/v2/storefront/option_type_serializer_decorator.rb +1 -1
  84. data/app/serializers/spree/v2/storefront/product_option_type_serializer.rb +15 -0
  85. data/app/serializers/spree/v2/storefront/product_serializer_decorator.rb +15 -0
  86. data/app/serializers/spree/v2/tenant/line_item_serializer.rb +1 -1
  87. data/app/serializers/spree/v2/tenant/option_type_serializer.rb +1 -1
  88. data/app/serializers/spree_cm_commissioner/v2/operator/dynamic_field_option_serializer.rb +9 -0
  89. data/app/serializers/spree_cm_commissioner/v2/operator/dynamic_field_serializer.rb +11 -0
  90. data/app/serializers/spree_cm_commissioner/v2/operator/guest_dynamic_field_serializer.rb +14 -0
  91. data/app/serializers/spree_cm_commissioner/v2/operator/guest_serializer.rb +1 -0
  92. data/app/serializers/spree_cm_commissioner/v2/storefront/accommodation_variant_serializer.rb +44 -0
  93. data/app/serializers/spree_cm_commissioner/v2/storefront/inventory_item_serializer.rb +4 -4
  94. data/app/serializers/spree_cm_commissioner/v2/storefront/rate_plan_serializer.rb +16 -0
  95. data/app/services/spree_cm_commissioner/integrations/book_me_bus_v1/polling/sync_item_availability.rb +1 -1
  96. data/app/services/spree_cm_commissioner/integrations/larryta/polling/sync_item_availability.rb +1 -1
  97. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/accept_push.rb +111 -0
  98. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/accommodation_lock.rb +43 -0
  99. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/apply_updates.rb +133 -0
  100. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/availability_snapshot.rb +127 -0
  101. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/inventory_update.rb +35 -0
  102. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/pushed_nights.rb +82 -0
  103. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/unseen_room_nights.rb +49 -0
  104. data/app/services/spree_cm_commissioner/integrations/switch_cm/inventory/validate_updates.rb +153 -0
  105. data/app/services/spree_cm_commissioner/integrations/switch_cm/reservations/feed.rb +209 -0
  106. data/app/services/spree_cm_commissioner/integrations/switch_cm/reservations/mark_seen.rb +62 -0
  107. data/app/services/spree_cm_commissioner/integrations/switch_cm/reservations/reservation.rb +171 -0
  108. data/app/services/spree_cm_commissioner/integrations/switch_cm/room_catalog.rb +88 -0
  109. data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_manager.rb +16 -0
  110. data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/full_sync_strategy.rb +39 -0
  111. data/app/services/spree_cm_commissioner/integrations/switch_cm/sync_strategies/incremental_sync_strategy.rb +51 -0
  112. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/availability_parser.rb +103 -0
  113. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/push_response.rb +45 -0
  114. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/reservation_serializer.rb +83 -0
  115. data/app/services/spree_cm_commissioner/integrations/switch_cm/xml/response_builder.rb +19 -0
  116. data/app/services/spree_cm_commissioner/integrations/vireak_buntham/polling/sync_item_availability.rb +1 -1
  117. data/app/services/spree_cm_commissioner/intercity_taxi_order/update.rb +1 -3
  118. data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_by_variant.rb +4 -0
  119. data/app/services/spree_cm_commissioner/inventory_items/reset.rb +4 -0
  120. data/app/services/spree_cm_commissioner/rate_plans/save.rb +65 -0
  121. data/app/services/spree_cm_commissioner/rates_availabilities/bulk_update.rb +225 -0
  122. data/app/services/spree_cm_commissioner/reschedule_requests/split_line_item.rb +5 -0
  123. data/app/services/spree_cm_commissioner/room_types/create.rb +87 -0
  124. data/app/services/spree_cm_commissioner/room_types/update_properties.rb +32 -0
  125. data/app/services/spree_cm_commissioner/room_types.rb +40 -0
  126. data/app/services/spree_cm_commissioner/route_prices/update_price_range.rb +3 -1
  127. data/app/services/spree_cm_commissioner/trips/search.rb +14 -7
  128. data/app/services/spree_cm_commissioner/trips/update_single_leg.rb +5 -1
  129. data/app/views/spree/admin/guests/_check_in_status.html.erb +28 -22
  130. data/app/views/spree/admin/guests/edit.html.erb +29 -26
  131. data/app/views/spree/admin/integrations/_form.html.erb +4 -0
  132. data/app/views/spree/admin/integrations/_switch_cm_fields.html.erb +53 -0
  133. data/app/views/spree/admin/inventory_items/_details.html.erb +56 -0
  134. data/app/views/spree/admin/product_option_types/index.html.erb +79 -0
  135. data/app/views/spree/admin/rate_plans/_form.html.erb +134 -0
  136. data/app/views/spree/admin/rate_plans/_order_form.html.erb +37 -0
  137. data/app/views/spree/admin/rates_availabilities/_block.html.erb +34 -0
  138. data/app/views/spree/admin/rates_availabilities/_bookings.html.erb +52 -0
  139. data/app/views/spree/admin/rates_availabilities/_filters.html.erb +48 -0
  140. data/app/views/spree/admin/rates_availabilities/_legend.html.erb +27 -0
  141. data/app/views/spree/admin/rates_availabilities/_list_pages.html.erb +14 -0
  142. data/app/views/spree/admin/rates_availabilities/_modal_header.html.erb +11 -0
  143. data/app/views/spree/admin/rates_availabilities/_names.html.erb +62 -0
  144. data/app/views/spree/admin/rates_availabilities/_script.html.erb +676 -0
  145. data/app/views/spree/admin/rates_availabilities/_styles.html.erb +159 -0
  146. data/app/views/spree/admin/rates_availabilities/index.html.erb +112 -0
  147. data/app/views/spree/admin/shared/sub_menu/_homepage.html.erb +0 -1
  148. data/app/views/spree/admin/stock_managements/_header.html.erb +0 -14
  149. data/app/views/spree/admin/stock_managements/_inventory_item_stats.html.erb +28 -0
  150. data/app/views/spree/admin/stock_managements/index.html.erb +1 -56
  151. data/config/initializers/spree_permitted_attributes.rb +1 -0
  152. data/config/locales/en.yml +241 -3
  153. data/config/locales/km.yml +241 -4
  154. data/config/routes.rb +36 -17
  155. data/db/migrate/20260928000000_add_oauth_application_id_to_cm_integrations.rb +5 -0
  156. data/db/migrate/20260929000000_create_cm_rate_plans.rb +22 -0
  157. data/db/migrate/20260929000001_add_rate_plan_to_spree_prices.rb +24 -0
  158. data/db/migrate/20260929000002_add_rate_plan_to_spree_line_items.rb +7 -0
  159. data/db/migrate/20260929000003_create_cm_option_value_rate_plans.rb +16 -0
  160. data/db/migrate/20261009000000_create_cm_rate_restrictions.rb +29 -0
  161. data/db/migrate/20261013000001_add_hidden_and_presentation_to_spree_product_option_types.rb +6 -0
  162. data/db/migrate/20261013000002_move_hidden_from_spree_option_types_to_product_option_types.rb +36 -0
  163. data/db/migrate/20261016000001_add_index_to_spree_line_items_number.rb +8 -0
  164. data/lib/spree_cm_commissioner/booking_dates.rb +43 -0
  165. data/lib/spree_cm_commissioner/calendar_event.rb +0 -25
  166. data/lib/spree_cm_commissioner/rate.rb +74 -0
  167. data/lib/spree_cm_commissioner/rate_closings.rb +48 -0
  168. data/lib/spree_cm_commissioner/rates_availability_grid.rb +113 -0
  169. data/lib/spree_cm_commissioner/stay_price.rb +30 -0
  170. data/lib/spree_cm_commissioner/stay_prices.rb +58 -0
  171. data/lib/spree_cm_commissioner/test_helper/factories/integration_factory.rb +6 -0
  172. data/lib/spree_cm_commissioner/test_helper/factories/option_type_factory.rb +6 -0
  173. data/lib/spree_cm_commissioner/test_helper/factories/option_value_rate_plan_factory.rb +6 -0
  174. data/lib/spree_cm_commissioner/test_helper/factories/rate_plan_factory.rb +14 -0
  175. data/lib/spree_cm_commissioner/test_helper/factories/variant_factory.rb +9 -0
  176. data/lib/spree_cm_commissioner/version.rb +1 -1
  177. data/lib/spree_cm_commissioner.rb +6 -0
  178. metadata +94 -32
  179. data/app/assets/javascripts/spree_cm_commissioner/calendar.js +0 -59
  180. data/app/controllers/spree/admin/calendars/base_controller.rb +0 -27
  181. data/app/controllers/spree/admin/calendars/orders_controller.rb +0 -27
  182. data/app/controllers/spree/admin/homepage_feed_controller.rb +0 -36
  183. data/app/controllers/spree/admin/metafields_controller.rb +0 -69
  184. data/app/models/concerns/spree_cm_commissioner/metafield.rb +0 -15
  185. data/app/models/spree_cm_commissioner/homepage_feed.rb +0 -4
  186. data/app/overrides/spree/admin/shared/_main_menu/sidebar_calendars.html.erb.deface +0 -5
  187. data/app/overrides/spree/admin/shared/_product_tabs/metafields.html.erb.deface +0 -15
  188. data/app/serializers/spree_cm_commissioner/v2/storefront/taxon_include_product_serializer.rb +0 -13
  189. data/app/serializers/spree_cm_commissioner/v2/storefront/vendor_include_product_serializer.rb +0 -14
  190. data/app/services/spree_cm_commissioner/feed/taxon_include_product.rb +0 -20
  191. data/app/services/spree_cm_commissioner/feed/taxon_product.rb +0 -54
  192. data/app/services/spree_cm_commissioner/feed/vendor_include_product.rb +0 -20
  193. data/app/services/spree_cm_commissioner/feed/vendor_product.rb +0 -52
  194. data/app/services/spree_cm_commissioner/homepage_data_loader.rb +0 -112
  195. data/app/services/spree_cm_commissioner/metafields/product_metadata_service.rb +0 -32
  196. data/app/views/shared/_calendar.html.erb +0 -27
  197. data/app/views/shared/calendar/_order_popover.html.erb +0 -18
  198. data/app/views/shared/calendar/_orders_sidebar.html.erb +0 -41
  199. data/app/views/spree/admin/calendars/orders/index.html.erb +0 -22
  200. data/app/views/spree/admin/homepage_feed/_form.html.erb +0 -24
  201. data/app/views/spree/admin/homepage_feed/edit.html.erb +0 -17
  202. data/app/views/spree/admin/inventory_items/prices.html.erb +0 -43
  203. data/app/views/spree/admin/inventory_items/stocks.html.erb +0 -34
  204. data/app/views/spree/admin/metafields/_private_metafields_form.html.erb +0 -71
  205. data/app/views/spree/admin/metafields/_public_metafields_form.html.erb +0 -64
  206. data/app/views/spree/admin/metafields/edit.html.erb +0 -27
  207. data/app/views/spree/admin/shared/sub_menu/_calendar.html.erb +0 -3
  208. data/app/views/spree/admin/stock_managements/calendar.html.erb +0 -310
@@ -31,8 +31,6 @@ module SpreeCmCommissioner
31
31
 
32
32
  validate :validate_trip_stops_for_scheduled_trips
33
33
 
34
- validates :passenger_count, numericality: { only_integer: true, greater_than: 0 }, allow_nil: true
35
-
36
34
  # Groups LineItems by trip fields. Some fields can be null,
37
35
  # so use it with caution. Output:
38
36
  # {
@@ -62,8 +60,6 @@ module SpreeCmCommissioner
62
60
  def drop_off_trip_stop_id = private_metadata['drop_off_trip_stop_id']&.to_i
63
61
  def service_origin_id = private_metadata['service_origin_id']&.to_i
64
62
 
65
- def passenger_count = public_metadata['passenger_count']&.to_i
66
-
67
63
  def pickup_map_place
68
64
  pickup_hash = public_metadata&.dig('pickup_map_place')
69
65
  return nil if pickup_hash.blank?
@@ -121,10 +117,6 @@ module SpreeCmCommissioner
121
117
  set_private_metadata_value('service_origin_id', value)
122
118
  end
123
119
 
124
- def passenger_count=(value)
125
- set_public_metadata_value('passenger_count', value)
126
- end
127
-
128
120
  private
129
121
 
130
122
  def resolve_service_origin_id
@@ -153,14 +145,6 @@ module SpreeCmCommissioner
153
145
  end
154
146
  end
155
147
 
156
- def set_public_metadata_value(key, value)
157
- if value.nil?
158
- public_metadata.delete(key)
159
- else
160
- public_metadata[key] = value.to_s
161
- end
162
- end
163
-
164
148
  def set_private_metadata_value(key, value)
165
149
  if value.nil?
166
150
  private_metadata.delete(key)
@@ -33,6 +33,7 @@ module SpreeCmCommissioner
33
33
  'month' => 'integer',
34
34
  'number-of-adults' => 'integer',
35
35
  'number-of-kids' => 'integer',
36
+ 'number-of-guests' => 'integer',
36
37
  'kids-age-max' => 'integer',
37
38
  'allowed-extra-adults' => 'integer',
38
39
  'allowed-extra-kids' => 'integer',
@@ -49,6 +50,13 @@ module SpreeCmCommissioner
49
50
  'top-up-amount' => 'integer'
50
51
  }.freeze
51
52
 
53
+ # System option types for rate plan terms. RatePlanOptions reads them by these names.
54
+ RATE_PLAN_RESERVED_OPTIONS = {
55
+ 'meal-plan' => 'string',
56
+ 'refundable' => 'boolean',
57
+ 'free-cancellation-days' => 'integer'
58
+ }.freeze
59
+
52
60
  included do
53
61
  include SpreeCmCommissioner::ParameterizeName
54
62
 
@@ -59,6 +67,10 @@ module SpreeCmCommissioner
59
67
 
60
68
  before_validation :set_reserved_options_attributes, if: :reserved_option?
61
69
 
70
+ # reserved_option? reads the saved name, so a new record never matches it; rate plan system types need
71
+ # their kind and attr_type from the start, since RatePlanOptions reads them by name.
72
+ before_validation :set_reserved_options_attributes, if: -> { new_record? && RATE_PLAN_RESERVED_OPTIONS.key?(name) }
73
+
62
74
  after_save :sort_date_time_option_values, if: -> { attr_type == 'date' || attr_type == 'time' }
63
75
  after_save :update_variants_metadata, if: :saved_change_to_name?
64
76
 
@@ -81,9 +93,10 @@ module SpreeCmCommissioner
81
93
  end
82
94
 
83
95
  def reserved_option?
84
- return name_was.in?(RESERVED_OPTIONS.keys) if name_changed?
96
+ reserved_names = RESERVED_OPTIONS.keys + RATE_PLAN_RESERVED_OPTIONS.keys
97
+ return name_was.in?(reserved_names) if name_changed?
85
98
 
86
- name.in?(RESERVED_OPTIONS.keys)
99
+ name.in?(reserved_names)
87
100
  end
88
101
 
89
102
  def ticket_type?
@@ -91,8 +104,13 @@ module SpreeCmCommissioner
91
104
  end
92
105
 
93
106
  def set_reserved_options_attributes
94
- self.attr_type = RESERVED_OPTIONS[name]
95
- self.kind = :variant
107
+ if RATE_PLAN_RESERVED_OPTIONS.key?(name)
108
+ self.attr_type = RATE_PLAN_RESERVED_OPTIONS[name]
109
+ self.kind = :rate_plan
110
+ else
111
+ self.attr_type = RESERVED_OPTIONS[name]
112
+ self.kind = :variant
113
+ end
96
114
  end
97
115
 
98
116
  def sort_date_time_option_values
@@ -0,0 +1,62 @@
1
+ module SpreeCmCommissioner
2
+ # Rate plan terms are option values (source of truth) cached as {option_type_name => option_value_name} in
3
+ # the `options` column. Follows VariantOptionsConcern, which caches in public_metadata only because
4
+ # spree_variants is a core table; cm_rate_plans is ours, so it gets a column.
5
+ module RatePlanOptionsConcern
6
+ extend ActiveSupport::Concern
7
+
8
+ included do
9
+ # Terms beyond price, e.g. Meal plan: Breakfast, from option types of the `rate_plan` kind.
10
+ has_many :option_value_rate_plans, class_name: 'SpreeCmCommissioner::OptionValueRatePlan', inverse_of: :rate_plan, dependent: :destroy
11
+ has_many :option_values, through: :option_value_rate_plans, class_name: 'Spree::OptionValue'
12
+
13
+ validate :option_values_are_rate_plan_kind
14
+ validate :one_option_value_per_option_type
15
+ validate :free_cancellation_only_when_refundable
16
+
17
+ # Before validation, so validations can read the typed cache instead of re-deriving it from option_values.
18
+ before_validation :cache_options
19
+
20
+ delegate :meal_plan, :refundable?, :free_cancellation_days, to: :options
21
+ end
22
+
23
+ # Read as RatePlanOptions, stored as a hash, like Route#route_stops.
24
+ def options
25
+ RatePlanOptions.from_hash(read_attribute(:options))
26
+ end
27
+
28
+ def options=(value)
29
+ self[:options] = value.to_h
30
+ end
31
+
32
+ def cache_options
33
+ self.options = option_values.each_with_object({}) do |option_value, hash|
34
+ hash[option_value.option_type.name] = option_value.name
35
+ end
36
+ end
37
+
38
+ # Reloads option values first: callers like OptionValueRatePlan may hold an instance loaded before the change.
39
+ def cache_options!
40
+ option_values.reset
41
+ cache_options
42
+ save!
43
+ end
44
+
45
+ private
46
+
47
+ def option_values_are_rate_plan_kind
48
+ errors.add(:option_values, :invalid_kind) unless option_values.all? { |value| value.option_type&.rate_plan? }
49
+ end
50
+
51
+ # A rate plan can't be both "Room only" and "Breakfast".
52
+ def one_option_value_per_option_type
53
+ errors.add(:option_values, :duplicate_option_type) if option_values.map(&:option_type_id).uniq.size != option_values.size
54
+ end
55
+
56
+ def free_cancellation_only_when_refundable
57
+ return if refundable? || free_cancellation_days.nil?
58
+
59
+ errors.add(:option_values, :free_cancellation_not_refundable)
60
+ end
61
+ end
62
+ end
@@ -2,9 +2,21 @@ module SpreeCmCommissioner
2
2
  module VariantOptionsConcern
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ # VariantOptions#number_of_guests in SQL (explicit number-of-guests, else adults defaulting
6
+ # to 1 plus kids defaulting to 0), so search matches the capacity the variant reports.
7
+ GUEST_CAPACITY_SQL = <<~SQL.squish.freeze
8
+ COALESCE(
9
+ CAST(spree_variants.public_metadata->'cm_options'->>'number-of-guests' AS INTEGER),
10
+ COALESCE(CAST(spree_variants.public_metadata->'cm_options'->>'number-of-adults' AS INTEGER), #{VariantOptions::DEFAULT_NUMBER_OF_ADULTS}) +
11
+ COALESCE(CAST(spree_variants.public_metadata->'cm_options'->>'number-of-kids' AS INTEGER), 0)
12
+ )
13
+ SQL
14
+
5
15
  included do
6
16
  before_save :set_options_to_public_metadata
7
17
 
18
+ scope :with_guest_capacity, -> (number_of_guests) { where("#{GUEST_CAPACITY_SQL} >= ?", number_of_guests) }
19
+
8
20
  delegate :location,
9
21
  :start_date,
10
22
  :start_time,
@@ -0,0 +1,81 @@
1
+ module SpreeCmCommissioner
2
+ # The base price lookups (price_in, default_price, price_for_date) override Spree's, so they stay in VariantDecorator.
3
+ module VariantRatePlannable
4
+ extend ActiveSupport::Concern
5
+
6
+ attr_writer :preloaded_stay_prices, :preloaded_rate_closings
7
+
8
+ included do
9
+ has_many :active_rate_plans, -> { active.order(:position) }, class_name: 'SpreeCmCommissioner::RatePlan', inverse_of: :variant
10
+ has_many :rate_plans, -> { order(:position) }, class_name: 'SpreeCmCommissioner::RatePlan', inverse_of: :variant, dependent: :destroy
11
+
12
+ # Variants with a rate open on every date, by the rule #rates lists them with: the base rate when there's no
13
+ # active rate plan, else any active one (a derived one also needs the base rate open, RateClosings). Rate-level
14
+ # only; bookable_on_every_date checks the room-nights themselves.
15
+ scope :with_open_rate_on_every_date, lambda { |dates|
16
+ closed = lambda do |rate_condition|
17
+ SpreeCmCommissioner::RateRestriction
18
+ .joins(:inventory_item)
19
+ .where(closed: true, cm_inventory_items: { inventory_date: dates })
20
+ .where('cm_inventory_items.variant_id = spree_variants.id')
21
+ .where(rate_condition)
22
+ .arel.exists
23
+ end
24
+
25
+ active_rate_plans = SpreeCmCommissioner::RatePlan.active.where('cm_rate_plans.variant_id = spree_variants.id')
26
+ base_open = active_rate_plans.arel.exists.not.and(closed.call('cm_rate_restrictions.rate_plan_id IS NULL').not)
27
+ own_or_followed = [
28
+ 'cm_rate_restrictions.rate_plan_id = cm_rate_plans.id OR (cm_rate_plans.kind = ? AND cm_rate_restrictions.rate_plan_id IS NULL)',
29
+ SpreeCmCommissioner::RatePlan.kinds[:derived]
30
+ ]
31
+
32
+ rate_plan_open = active_rate_plans.where(closed.call(own_or_followed).not).arel.exists
33
+ where(base_open.or(rate_plan_open))
34
+ }
35
+ end
36
+
37
+ class_methods do
38
+ # Every variant gets the same batch: one per variant would be a price query per room. Each one only reads its
39
+ # own [variant_id, rate_plan_id] keys, so it never sees another room's prices. Expects active_rate_plans
40
+ # preloaded, or this adds a query per variant.
41
+ def preload_stay_prices(variants, dates:, currency:)
42
+ stay_prices = SpreeCmCommissioner::StayPrices.new(
43
+ variants: variants,
44
+ rate_plans: variants.flat_map(&:active_rate_plans),
45
+ dates: dates,
46
+ currency: currency
47
+ )
48
+
49
+ rate_closings = SpreeCmCommissioner::RateClosings.new(
50
+ variant_ids: variants.map(&:id),
51
+ dates: dates
52
+ )
53
+
54
+ variants.each do |variant|
55
+ variant.preloaded_stay_prices = stay_prices
56
+ variant.preloaded_rate_closings = rate_closings
57
+ end
58
+ end
59
+ end
60
+
61
+ # The default rate (the variant's own prices) only when there's no active rate plan. A hotel that also wants
62
+ # to sell at the plain price adds a 0% rate plan. Rates closed on any of the dates are left out.
63
+ def rates(dates:, currency:)
64
+ stay_prices = @preloaded_stay_prices if @preloaded_stay_prices&.matches?(dates: dates, currency: currency)
65
+ stay_prices ||= SpreeCmCommissioner::StayPrices.new(variants: [self], rate_plans: active_rate_plans, dates: dates, currency: currency)
66
+
67
+ rate_closings = @preloaded_rate_closings if @preloaded_rate_closings&.matches?(dates: dates)
68
+ rate_closings ||= SpreeCmCommissioner::RateClosings.new(variant_ids: [id], dates: dates)
69
+
70
+ (active_rate_plans.presence || [nil]).filter_map do |rate_plan|
71
+ next if rate_closings.closed?(id, rate_plan)
72
+
73
+ SpreeCmCommissioner::Rate.new(variant: self, rate_plan: rate_plan, dates: dates, currency: currency, stay_prices: stay_prices)
74
+ end
75
+ end
76
+
77
+ def stay_price(dates:, currency:)
78
+ SpreeCmCommissioner::StayPrices.new(variants: [self], rate_plans: [], dates: dates, currency: currency).for_variant(self)
79
+ end
80
+ end
81
+ end
@@ -6,6 +6,8 @@ module SpreeCmCommissioner
6
6
  base.validates :address1, presence: true, if: :require_address1?
7
7
  base.validates :city, presence: true, if: :require_city?
8
8
  base.validates :country, presence: true, if: :require_country?
9
+ # Country is optional, so redefine Spree's country delegation to return nil instead of raising.
10
+ base.delegate :name, :iso3, :iso, :iso_name, to: :country, prefix: true, allow_nil: true
9
11
  end
10
12
 
11
13
  # override to return false instead of true
@@ -8,12 +8,6 @@ module SpreeCmCommissioner
8
8
  # preference :favorite_number, :integer
9
9
  # preference :supported_locales, :array, default: [:en]
10
10
  preference :enabled, :boolean, default: true
11
- preference :displayed_product_taxon_ids, :array, default: []
12
- preference :top_category_taxon_ids, :array, default: []
13
- preference :trending_category_taxon_ids, :array, default: []
14
- preference :featured_brand_taxon_ids, :array, default: []
15
- preference :featured_vendor_ids, :array, default: []
16
- preference :cleared_cache_at, :string
17
11
  preference :prepaid_topup_min_amount, :decimal, default: 1
18
12
  preference :prepaid_topup_max_amount, :decimal, default: 500
19
13
 
@@ -19,7 +19,7 @@ module SpreeCmCommissioner
19
19
  # Lets Excel detect UTF-8 so Khmer names render correctly.
20
20
  UTF8_BOM = "\uFEFF".freeze
21
21
  ORDER_BATCH_SIZE = 200
22
- HEADERS = ['Guest', 'Phone Number', 'Order ID', 'Order Status', 'Ticket', 'Order Ticket', 'Guest Ticket'].freeze
22
+ HEADERS = ['Guest', 'Phone Number', 'Order ID', 'Order Status', 'Ticket', 'QR Data', 'Order Ticket', 'Guest Ticket'].freeze
23
23
 
24
24
  store_public_metadata :guests_count, :integer, default: 0
25
25
  store_private_metadata :exported_by_id, :integer
@@ -85,6 +85,7 @@ module SpreeCmCommissioner
85
85
  order.number,
86
86
  order.state&.capitalize,
87
87
  line_item.name,
88
+ order.qr_data,
88
89
  app_url_helpers.order_page_url(order.qr_data, **app_url_options),
89
90
  guest_ticket_link(guest, line_item)
90
91
  ]
@@ -3,7 +3,9 @@ module SpreeCmCommissioner
3
3
  class OperatorGuestJsonGzip < ::SpreeCmCommissioner::Export
4
4
  BATCH_INCLUDES = [
5
5
  :occupation, :user,
6
- { check_ins: %i[check_in_by check_in_session], line_item: [:variant, { order: :bill_address }] }
6
+ { check_ins: %i[check_in_by check_in_session], line_item: [:variant, { order: :bill_address }],
7
+ guest_dynamic_fields: [:dynamic_field_option, { dynamic_field: :dynamic_field_options }]
8
+ }
7
9
  ].freeze
8
10
 
9
11
  RESOURCE_INCLUDES = [
@@ -14,7 +16,11 @@ module SpreeCmCommissioner
14
16
  'line_item',
15
17
  'line_item.order',
16
18
  'line_item.order.billing_address',
17
- 'line_item.variant'
19
+ 'line_item.variant',
20
+ 'guest_dynamic_fields',
21
+ 'guest_dynamic_fields.dynamic_field_option',
22
+ 'guest_dynamic_fields.dynamic_field',
23
+ 'guest_dynamic_fields.dynamic_field.dynamic_field_options'
18
24
  ].freeze
19
25
 
20
26
  # Check-in group scope: one role, one export. `role` is the requesting operator's check-in group
@@ -83,7 +83,7 @@ module SpreeCmCommissioner
83
83
  self.whitelisted_ransackable_attributes = %w[first_name last_name phone_number gender contact occupation_id card_type created_at check_in_status]
84
84
 
85
85
  scope :with_dynamic_fields_and_options, lambda {
86
- includes(guest_dynamic_fields: { dynamic_field: :dynamic_field_options })
86
+ includes(guest_dynamic_fields: [:dynamic_field_option, { dynamic_field: :dynamic_field_options }])
87
87
  }
88
88
 
89
89
  def self.csv_importable_columns
@@ -14,10 +14,25 @@ module SpreeCmCommissioner
14
14
  validates :incremental_sync_interval_seconds, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 3600 }
15
15
  validates :full_sync_interval_hours, presence: true, numericality: { only_integer: true, greater_than: 0, less_than_or_equal_to: 168 }
16
16
 
17
+ # Listed from the folder rather than `descendants`, which misses subclasses Zeitwerk hasn't
18
+ # loaded yet in dev and test.
19
+ def self.types
20
+ Dir[SpreeCmCommissioner::Engine.root.join('app/models/spree_cm_commissioner/integrations/*.rb')]
21
+ .map { |path| "SpreeCmCommissioner::Integrations::#{File.basename(path, '.rb').camelize}" }
22
+ .sort
23
+ end
24
+
17
25
  def sync_manager
18
26
  raise NotImplementedError, 'Subclasses must implement the sync_manager method'
19
27
  end
20
28
 
29
+ # Whether the partner, not BookMe+, sets this variant's per-night stock, prices and closings.
30
+ # When true, stock resets and dashboard edits leave its inventory items alone, and new nights
31
+ # start closed until the partner opens them (InventoryItem). Off by default.
32
+ def inventory_external_wins?(_variant)
33
+ false
34
+ end
35
+
21
36
  def restock_external_inventory!(_order, _line_items)
22
37
  raise NotImplementedError, 'Subclasses must implement the restock_external_inventory! method'
23
38
  end
@@ -0,0 +1,96 @@
1
+ # A hotel connected to switch.cm, which calls our Booking.com-compatible channel-manager API
2
+ # (Spree::Api::SwitchCm) instead of us calling it: it pushes availability, prices and
3
+ # restrictions, and pulls reservations. So there's nothing to pull, hold or unstock externally;
4
+ # polling only runs our own checks (SyncManager).
5
+ #
6
+ # Every switch.cm row shares one Doorkeeper app; the hotels switch.cm may manage are the rows
7
+ # pointing at it. `paused` is the setup state: switch.cm can read the hotel (room list,
8
+ # reservation summary) while it keeps selling on its own counts. `active` with conflict strategy
9
+ # `external_wins` hands the rooms' availability, prices and restrictions over to switch.cm.
10
+ class SpreeCmCommissioner::Integrations::SwitchCm < SpreeCmCommissioner::Integration
11
+ READABLE_STATUSES = %i[active paused].freeze
12
+
13
+ belongs_to :oauth_application, class_name: 'Spree::OauthApplication', optional: false
14
+
15
+ # Reservations completed before this aren't in the reservation feeds: switch.cm imports them
16
+ # through the reservation summary while setting the hotel up.
17
+ store_public_metadata :connected_at, :datetime
18
+ alias stored_connected_at connected_at
19
+
20
+ before_create :set_connected_at
21
+ after_commit :sync_unpushed_nights, on: %i[create update]
22
+
23
+ # Polling runs the hotel's periodic checks (SyncManager), and the incremental one alerts on
24
+ # every run while reservations sit unpulled, so the column's 10-second default would flood
25
+ # Telegram. Full runs daily upkeep.
26
+ attribute :incremental_sync_interval_seconds, :integer, default: -> { SyncStrategies::IncrementalSyncStrategy::UNSEEN_AFTER.to_i }
27
+
28
+ scope :readable, -> { where(status: READABLE_STATUSES) }
29
+
30
+ # override
31
+ def sync_manager
32
+ SyncManager.new(integration: self)
33
+ end
34
+
35
+ # Only for rooms switch.cm can see (RoomCatalog): a hotel's other products, and rooms that need
36
+ # confirmation, keep BookMe+ counts since switch.cm never pushes them.
37
+ # override
38
+ def inventory_external_wins?(variant)
39
+ owns_inventory? && variant.accommodation? && !variant.need_confirmation?
40
+ end
41
+
42
+ def owns_inventory?
43
+ active? && external_wins?
44
+ end
45
+
46
+ # switch.cm pulls reservations, so a BookMe+ sale needs nothing from it at checkout, and the
47
+ # hotel's own cancellation rules decide whether a booking can be cancelled.
48
+ # override
49
+ def unstock_external_inventory!(_order, _line_items); end
50
+
51
+ # override
52
+ def restock_external_inventory!(_order, _line_items); end
53
+
54
+ # override
55
+ def sync_item_availability!(_options = {}); end
56
+
57
+ # override
58
+ def external_inventory_items(_order)
59
+ []
60
+ end
61
+
62
+ # override
63
+ def external_inventory_item_status(_order, _options)
64
+ {}
65
+ end
66
+
67
+ # A row saved without this class's callbacks (e.g. created as the base Integration) has no stamp;
68
+ # nil would hide every reservation from the feeds and from UnseenRoomNights, so it falls back to
69
+ # the row's creation time, which is what the stamp would have been.
70
+ def connected_at
71
+ stored_connected_at || created_at
72
+ end
73
+
74
+ # While switch.cm owns the rooms, nights it hasn't pushed stay closed, so the admin page shows
75
+ # this until the first full push lands.
76
+ def push_progress
77
+ Inventory::PushedNights.new(self).progress
78
+ end
79
+
80
+ private
81
+
82
+ # Checks the stored value, not #connected_at: Rails sets created_at before create callbacks run,
83
+ # so the fallback would already answer and the stamp would never be written.
84
+ def set_connected_at
85
+ self.connected_at = Time.current.iso8601 if stored_connected_at.nil?
86
+ end
87
+
88
+ # Taking over closes the rooms' unpushed nights; letting go (paused, disconnected, or another
89
+ # conflict strategy) reopens them, while pushed nights keep switch.cm's last numbers.
90
+ def sync_unpushed_nights
91
+ owned_before = status_before_last_save.to_s == 'active' && conflict_strategy_before_last_save.to_s == 'external_wins'
92
+ return if owned_before == owns_inventory?
93
+
94
+ Inventory::PushedNights.new(self).reconcile_unpushed!
95
+ end
96
+ end
@@ -16,6 +16,11 @@ module SpreeCmCommissioner
16
16
 
17
17
  has_many :prices, class_name: 'Spree::Price', inverse_of: :inventory_item, dependent: :destroy
18
18
 
19
+ # The variant's own prices for this date. A rate plan stores its date price on the same
20
+ # inventory item, so `prices` can hold several rows per currency; reading or writing through `prices`
21
+ # would show or overwrite a rate plan's price as the variant's. Rows built here get `rate_plan_id: nil`.
22
+ has_many :variant_prices, -> { where(rate_plan_id: nil) }, class_name: 'Spree::Price', inverse_of: :inventory_item
23
+
19
24
  # Scoped to `unavailable` (not `reserved_or_on_hold`) so operator-locked seats are exposed
20
25
  # everywhere a sold seat is — including the storefront API, which is what lets locked seats render
21
26
  # as unavailable in the app with no client change.
@@ -27,6 +32,9 @@ module SpreeCmCommissioner
27
32
 
28
33
  has_many :lock_movements, class_name: 'SpreeCmCommissioner::InventoryLockMovement', inverse_of: :inventory_item
29
34
 
35
+ # Closings of single rates on this date; closing every rate is `closed`.
36
+ has_many :rate_restrictions, class_name: 'SpreeCmCommissioner::RateRestriction', inverse_of: :inventory_item, dependent: :delete_all
37
+
30
38
  # Validation
31
39
  validates :quantity_available, numericality: { greater_than_or_equal_to: 0 }
32
40
  validates :quantity_on_hold, numericality: { greater_than_or_equal_to: 0 }
@@ -34,13 +42,25 @@ module SpreeCmCommissioner
34
42
  validates :quantity_locked_on_hold, numericality: { greater_than_or_equal_to: 0 }
35
43
  validates :max_capacity, numericality: { greater_than_or_equal_to: 0 } # Originally inventory of each variant.
36
44
  validates :inventory_date, presence: true, if: :permanent_stock?
37
- validates :variant_id, uniqueness: { scope: :inventory_date, message: -> (object, _data) { "The variant is taken on #{object.inventory_date}" } }
45
+ # Create only: most saves just touch stock quantities, and the key below can't change afterwards. Still needed
46
+ # for NULL inventory_date rows, which the unique index does not cover.
47
+ validates :variant_id, uniqueness: { scope: :inventory_date, message: -> (object, _data) { "The variant is taken on #{object.inventory_date}" } },
48
+ on: :create
49
+ validate :variant_and_inventory_date_unchanged, on: :update
38
50
 
39
51
  # Scope
40
52
  scope :active, -> { where(inventory_date: nil).or(where('inventory_date >= ?', Time.zone.today)) }
41
53
 
54
+ # Same rule as CachedInventoryItem#sellable?, which booking checks: open for sale and not sold out.
55
+ scope :bookable_on, -> (dates) { where(inventory_date: dates, closed: false).where('quantity_available > 0') }
56
+
42
57
  before_save -> { self.product_type = variant.product_type }, if: -> { product_type.nil? }
43
58
 
59
+ # A night whose inventory a partner owns (Integration#inventory_external_wins?) starts closed: its
60
+ # count is only our seed from the physical room count, which the partner may already have sold
61
+ # elsewhere. The partner's first availability push opens it.
62
+ before_create -> { self.closed = true }, if: -> { permanent_stock? && variant&.inventory_external_wins? }
63
+
44
64
  after_commit :schedule_product_cache_invalidation
45
65
  after_commit :publish_live_stock_status_to_firestore
46
66
 
@@ -95,8 +115,10 @@ module SpreeCmCommissioner
95
115
  available_before.to_i + locked_before.to_i
96
116
  end
97
117
 
118
+ # nil above MAX_DISPLAY_STOCK: "3 left" helps a guest decide, "20 left" when 300 are free misleads. Apps read
119
+ # nil as plenty, not sold out.
98
120
  def public_quantity_available
99
- [quantity_available, MAX_DISPLAY_STOCK].min
121
+ quantity_available if quantity_available <= MAX_DISPLAY_STOCK
100
122
  end
101
123
 
102
124
  # How many seats an operator has taken off sale for this date.
@@ -108,7 +130,7 @@ module SpreeCmCommissioner
108
130
  end
109
131
 
110
132
  def price_in(currency)
111
- prices.detect { |price| price.currency == currency } || prices.build(currency: currency)
133
+ variant_prices.detect { |price| price.currency == currency } || variant_prices.build(currency: currency)
112
134
  end
113
135
 
114
136
  # This method is only used when admin update stock
@@ -194,5 +216,12 @@ module SpreeCmCommissioner
194
216
  expires_at = [inventory_date.end_of_day + 7.days, 1.day.from_now].max
195
217
  (expires_at - Time.zone.now).to_i
196
218
  end
219
+
220
+ private
221
+
222
+ def variant_and_inventory_date_unchanged
223
+ errors.add(:variant_id, :cannot_be_changed) if will_save_change_to_variant_id?
224
+ errors.add(:inventory_date, :cannot_be_changed) if will_save_change_to_inventory_date?
225
+ end
197
226
  end
198
227
  end
@@ -95,6 +95,7 @@ module SpreeCmCommissioner
95
95
  base.include SpreeCmCommissioner::ProductDelegation
96
96
  base.include SpreeCmCommissioner::LineItemOpenDatedTrippable
97
97
  base.include SpreeCmCommissioner::LineItemBundleable
98
+ base.include SpreeCmCommissioner::LineItemRatePlannable
98
99
  base.include SpreeCmCommissioner::KycBitwise
99
100
  end
100
101
 
@@ -118,21 +119,21 @@ module SpreeCmCommissioner
118
119
  self.product_type = variant.product_type
119
120
  self.from_date = opts.delete(:from_date)&.to_datetime
120
121
  self.to_date = opts.delete(:to_date)&.to_datetime
122
+ self.rate_plan_id = opts.delete(:rate_plan_id).presence || rate_plan_id
121
123
 
122
124
  base_price = variant.price_in(currency)
123
125
 
124
126
  self.price = if permanent_stock?
125
127
  date_range.map do |date|
126
- selected_price = variant.price_for_date(date: date, currency: currency) || base_price
127
- (selected_price.amount || 0) + variant.price_modifier_amount_in(currency, opts.merge(date: date))
128
+ (unit_price_amount(currency, base_price, date: date) || 0) + variant.price_modifier_amount_in(currency, opts.merge(date: date))
128
129
  end.sum
129
130
  else
130
- (base_price.amount || 0) + variant.price_modifier_amount_in(currency, opts)
131
+ (unit_price_amount(currency, base_price) || 0) + variant.price_modifier_amount_in(currency, opts)
131
132
  end
132
133
  end
133
134
 
134
135
  # override to calculate price per date for permanent stock
135
- def update_price # rubocop:disable Metrics/PerceivedComplexity
136
+ def update_price
136
137
  # Skip price updates for ticket transfer orders to preserve custom transfer pricing
137
138
  return if order&.ticket_transfer?
138
139
  # prepaid_top_up line items carry a one-time custom_price set at creation (see
@@ -144,11 +145,10 @@ module SpreeCmCommissioner
144
145
  base_price = variant.price_in(order.currency)
145
146
  self.price = if permanent_stock?
146
147
  date_range.map do |date|
147
- selected_price = variant.price_for_date(date: date, currency: order.currency) || base_price
148
- selected_price.amount || 0
148
+ unit_price_amount(order.currency, base_price, date: date) || 0
149
149
  end.sum
150
150
  else
151
- base_price.amount || 0
151
+ unit_price_amount(order.currency, base_price) || 0
152
152
  end
153
153
  end
154
154
 
@@ -3,13 +3,29 @@ module SpreeCmCommissioner
3
3
  module MaintenanceTasks
4
4
  class CacheInvalidation < MaintenanceTask
5
5
  def maintain
6
- return if ENV['CACHE_AUTO_INVALIDATION_DISABLED'] == 'yes'
6
+ if ENV['CACHE_AUTO_INVALIDATION_DISABLED'] == 'yes'
7
+ log_invalidation('skipped: CACHE_AUTO_INVALIDATION_DISABLED')
8
+ return
9
+ end
10
+
11
+ result = SpreeCmCommissioner::ApiCaches::Invalidate.call(model: maintainable.class.name, id: maintainable.id)
7
12
 
8
- if maintainable.is_a?(SpreeCmCommissioner::HomepageBackground) || maintainable.is_a?(Spree::Menu)
9
- SpreeCmCommissioner::HomepageDataLoader.clear_cache
13
+ if result.success?
14
+ log_invalidation('sent', patterns: result.value[:api_patterns])
15
+ else
16
+ log_invalidation("failed: #{result.error}")
10
17
  end
18
+ end
19
+
20
+ private
11
21
 
12
- SpreeCmCommissioner::ApiCaches::Invalidate.call(model: maintainable.class.name, id: maintainable.id)
22
+ # Each path costs money on CloudFront, so every outcome is logged and kept on the task for troubleshooting.
23
+ def log_invalidation(outcome, patterns: [])
24
+ log_run("Invalidation #{outcome} #{patterns.join(', ')}".strip)
25
+ CmAppLogger.log(
26
+ label: "#{self.class.name} #{outcome}",
27
+ data: { maintenance_task_id: id, maintainable_type: maintainable_type, maintainable_id: maintainable_id, patterns: patterns }
28
+ )
13
29
  end
14
30
  end
15
31
  end
@@ -26,7 +26,7 @@ module SpreeCmCommissioner
26
26
 
27
27
  # Denormalises maintainable's price range onto EventMetadata#min_price/max_price/currency
28
28
  # so the event-details bar can show "from $X" without loading the ticket list. Triggered
29
- # by PriceDecorator's after_commit hook via a pending task, and run unconditionally here.
29
+ # by PriceDecorator's after_commit hook via a pending task. Only saves when the range moved.
30
30
  def refresh_price_range
31
31
  currency = Spree::Store.default.default_currency
32
32
 
@@ -35,11 +35,12 @@ module SpreeCmCommissioner
35
35
  .where(variant_id: priced_variant_ids, currency: currency)
36
36
  .pick(Arel.sql('MIN(amount)'), Arel.sql('MAX(amount)'))
37
37
 
38
- maintainable.update!(
38
+ maintainable.assign_attributes(
39
39
  min_price: min_amount&.to_s,
40
40
  max_price: max_amount&.to_s,
41
41
  currency: min_amount.present? ? currency : nil
42
42
  )
43
+ maintainable.save! if maintainable.changed?
43
44
  end
44
45
 
45
46
  def update_conversion_for(product_taxon)