spree_cm_commissioner 2.11.0 → 2.12.0.pre.pre2
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/Gemfile.lock +1 -1
- data/app/assets/javascripts/spree_cm_commissioner/backend.js +1 -0
- data/app/assets/javascripts/spree_cm_commissioner/copy_to_clipboard.js +49 -0
- data/app/controllers/spree/admin/inventory_locks_controller.rb +134 -0
- data/app/controllers/spree/admin/private_sales_controller.rb +116 -0
- data/app/controllers/spree/api/v2/operator/check_ins_controller.rb +22 -4
- data/app/controllers/spree/api/v2/operator/dashboard_crew_events_controller.rb +1 -1
- data/app/controllers/spree/api/v2/operator/event_charts_controller.rb +34 -12
- data/app/controllers/spree/api/v2/resource_controller_decorator.rb +7 -0
- data/app/controllers/spree/api/v2/storefront/draft_orders_controller.rb +6 -1
- data/app/controllers/spree/api/v2/storefront/private_sale_products_controller.rb +34 -0
- data/app/controllers/spree/api/v2/storefront/queue_draft_orders_controller.rb +60 -0
- data/app/controllers/spree/api/v2/storefront/waiting_room_sessions_controller.rb +4 -1
- data/app/controllers/spree/api/v2/tenant/draft_orders_controller.rb +6 -1
- data/app/interactors/spree_cm_commissioner/payment_incomplete_notification_sender.rb +12 -1
- data/app/interactors/spree_cm_commissioner/waiting_room_session_creator.rb +57 -8
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +8 -4
- data/app/jobs/spree_cm_commissioner/queue_draft_orders/process_job.rb +23 -0
- data/app/jobs/spree_cm_commissioner/stock_status_poll_job.rb +26 -0
- data/app/models/concerns/spree_cm_commissioner/event_metadata.rb +2 -0
- data/app/models/spree_cm_commissioner/inventory_item.rb +27 -10
- data/app/models/spree_cm_commissioner/line_item_decorator.rb +12 -1
- data/app/models/spree_cm_commissioner/private_sale.rb +106 -0
- data/app/models/spree_cm_commissioner/redis_stock/cached_inventory_items_builder.rb +38 -19
- data/app/models/spree_cm_commissioner/stock/availability_checker.rb +15 -7
- data/app/models/spree_cm_commissioner/stock/availability_validator_decorator.rb +57 -5
- data/app/models/spree_cm_commissioner/stock/line_item_availability_checker.rb +7 -4
- data/app/models/spree_cm_commissioner/stock/order_availability_checker.rb +7 -1
- data/app/models/spree_cm_commissioner/taxon_decorator.rb +25 -0
- data/app/models/spree_cm_commissioner/variant_decorator.rb +5 -1
- data/app/models/spree_cm_commissioner/waiting_room_session.rb +9 -0
- data/app/overrides/spree/admin/shared/sub_menu/_product/private_sales_tab.html.erb.deface +3 -0
- data/app/overrides/spree/admin/taxons/_form/live_stock_window_form_date.html.erb.deface +41 -0
- data/app/queries/spree_cm_commissioner/dashboard_crew_event_query.rb +10 -5
- data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +7 -1
- data/app/services/spree_cm_commissioner/cart/add_guest.rb +12 -7
- data/app/services/spree_cm_commissioner/draft_orders/create.rb +37 -1
- data/app/services/spree_cm_commissioner/events/publish_stock_statuses_to_firestore.rb +178 -0
- data/app/services/spree_cm_commissioner/inventory_holds/acquire.rb +27 -12
- data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +10 -9
- data/app/services/spree_cm_commissioner/inventory_holds/release.rb +12 -11
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities.rb +30 -9
- data/app/services/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold.rb +33 -8
- data/app/services/spree_cm_commissioner/inventory_items/move_to_locked.rb +159 -0
- data/app/services/spree_cm_commissioner/inventory_items/reset.rb +38 -18
- data/app/services/spree_cm_commissioner/private_sales/authorize.rb +41 -0
- data/app/services/spree_cm_commissioner/queue_draft_orders/enqueue.rb +64 -0
- data/app/services/spree_cm_commissioner/queue_draft_orders/process.rb +81 -0
- data/app/services/spree_cm_commissioner/queue_draft_orders/publish_status.rb +39 -0
- data/app/services/spree_cm_commissioner/redis_stock/restock.rb +16 -5
- data/app/services/spree_cm_commissioner/redis_stock/unstock.rb +12 -5
- data/app/services/spree_cm_commissioner/waiting_room/bypass_eligibility.rb +66 -0
- data/app/views/spree/admin/inventory_locks/index.html.erb +70 -0
- data/app/views/spree/admin/private_sales/_form.html.erb +57 -0
- data/app/views/spree/admin/private_sales/_share_link.html.erb +24 -0
- data/app/views/spree/admin/private_sales/_stock.html.erb +83 -0
- data/app/views/spree/admin/private_sales/edit.html.erb +31 -0
- data/app/views/spree/admin/private_sales/index.html.erb +80 -0
- data/app/views/spree/admin/private_sales/new.html.erb +14 -0
- data/app/views/spree/admin/stock_managements/index.html.erb +8 -0
- data/config/initializers/spree_permitted_attributes.rb +2 -0
- data/config/locales/en.yml +90 -4
- data/config/locales/km.yml +12 -1
- data/config/routes.rb +12 -0
- data/db/migrate/20260820000000_add_locked_quantities_to_cm_inventory_items.rb +12 -0
- data/db/migrate/20260820100000_create_cm_private_sales.rb +27 -0
- data/db/migrate/20260824120000_add_user_id_to_cm_waiting_room_sessions.rb +6 -0
- data/db/migrate/20260825092416_add_live_stock_window_to_spree_taxons.rb +8 -0
- data/lib/spree_cm_commissioner/cached_inventory_item.rb +9 -0
- data/lib/spree_cm_commissioner/test_helper/factories/private_sale_factory.rb +25 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- metadata +33 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c7223999a2cd0d551f0694c7cd4103195db0ca0473b7ee577a17be13d6686f5f
|
|
4
|
+
data.tar.gz: bf3fa4a68bf67915a77bd37b2cc2fdaa7f05bece78be8101b76c786f9d8daad5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e4b196cc0f1426f4672f511546509ddcbf41981203f8affb32e26df9c2e2f0d08cefefb2530810405fa8d9cdf15f3a680e6ea78fcf0751b0efe7dba25e7ce8d
|
|
7
|
+
data.tar.gz: 9de8a0f14b88d42174f53d13cf3ac2cf0fd7cf5aea024a31f8e5c8a76aa86dc0dc40ac2f6d5774d7ecc35ca3a54868e4f48fb5084321bbb0c8066138a304669f
|
data/Gemfile.lock
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Copy-to-clipboard for any element carrying data-copy-value.
|
|
2
|
+
//
|
|
3
|
+
// Delegated from document rather than bound per button: the private sales index renders one of these
|
|
4
|
+
// per row, and re-binding on every Turbo page load would stack duplicate listeners on the same nodes.
|
|
5
|
+
//
|
|
6
|
+
// The icon swap is the whole feedback mechanism — without it a click looks like nothing happened.
|
|
7
|
+
document.addEventListener("click", function (event) {
|
|
8
|
+
const trigger = event.target.closest("[data-copy-value]");
|
|
9
|
+
if (!trigger) return;
|
|
10
|
+
|
|
11
|
+
event.preventDefault();
|
|
12
|
+
copyText(trigger.dataset.copyValue).then(function () {
|
|
13
|
+
confirmCopied(trigger);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// navigator.clipboard needs a secure context, which rules it out on plain-http staging boxes.
|
|
18
|
+
// execCommand is deprecated but is the only thing that works there, so it stays as the fallback.
|
|
19
|
+
function copyText(value) {
|
|
20
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
21
|
+
return navigator.clipboard.writeText(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const scratch = document.createElement("textarea");
|
|
25
|
+
scratch.value = value;
|
|
26
|
+
scratch.setAttribute("readonly", "");
|
|
27
|
+
scratch.style.position = "fixed";
|
|
28
|
+
scratch.style.opacity = "0";
|
|
29
|
+
document.body.appendChild(scratch);
|
|
30
|
+
scratch.select();
|
|
31
|
+
document.execCommand("copy");
|
|
32
|
+
document.body.removeChild(scratch);
|
|
33
|
+
|
|
34
|
+
return Promise.resolve();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function confirmCopied(trigger) {
|
|
38
|
+
const done = trigger.querySelector("[data-copy-done]");
|
|
39
|
+
const idle = trigger.querySelector("[data-copy-idle]");
|
|
40
|
+
if (!done || !idle) return;
|
|
41
|
+
|
|
42
|
+
idle.style.display = "none";
|
|
43
|
+
done.style.display = "inline";
|
|
44
|
+
|
|
45
|
+
setTimeout(function () {
|
|
46
|
+
idle.style.display = "inline";
|
|
47
|
+
done.style.display = "none";
|
|
48
|
+
}, 1500);
|
|
49
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
# Withholds units of one product from public sale.
|
|
4
|
+
#
|
|
5
|
+
# Reached only from a private sale — there is no tab or sidebar entry. `create` is the POST target
|
|
6
|
+
# for the per-variant inputs on Spree::Admin::PrivateSalesController#edit, and `index` is the
|
|
7
|
+
# date-range screen that page links to for permanent-stock products, where one variant has an
|
|
8
|
+
# inventory item per date and a single input cannot express the range.
|
|
9
|
+
#
|
|
10
|
+
# All the accounting lives in InventoryItems::MoveToLocked — this controller resolves the
|
|
11
|
+
# product, calls it, and turns its named failures into flash messages.
|
|
12
|
+
#
|
|
13
|
+
class InventoryLocksController < Spree::Admin::ResourceController
|
|
14
|
+
skip_before_action :load_resource
|
|
15
|
+
|
|
16
|
+
before_action :load_parent
|
|
17
|
+
|
|
18
|
+
# GET /admin/products/:product_id/inventory_locks
|
|
19
|
+
def index
|
|
20
|
+
# Only active items can be locked — a past date cannot be sold, so withholding from it
|
|
21
|
+
# protects nothing (MoveToLocked refuses it outright).
|
|
22
|
+
# Paginated like every other admin index here: a permanent-stock product has one row per
|
|
23
|
+
# variant per date, so an unpaginated page is thousands of rows.
|
|
24
|
+
@inventory_items = @product.inventory_items.active
|
|
25
|
+
.includes(variant: { option_values: :option_type })
|
|
26
|
+
.order(:inventory_date)
|
|
27
|
+
.page(params[:page]).per(50)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# POST /admin/products/:product_id/inventory_locks
|
|
31
|
+
#
|
|
32
|
+
# One action for both directions: a positive quantity locks, a negative one releases. Applied
|
|
33
|
+
# across a date range for permanent-stock products, where each date is its own inventory item —
|
|
34
|
+
# "lock 5" means 5 per date, not 5 across the range.
|
|
35
|
+
def create
|
|
36
|
+
items = target_inventory_items
|
|
37
|
+
return redirect_with_error(:invalid_date_range) if items.nil?
|
|
38
|
+
return redirect_with_error(:no_inventory_item) if items.empty?
|
|
39
|
+
|
|
40
|
+
moved, failed = items.map { |item| move(item) }.partition(&:success?)
|
|
41
|
+
return redirect_with_error(failed.first.error.value) if moved.empty?
|
|
42
|
+
|
|
43
|
+
# Each date is moved independently and nothing can undo one that already went through —
|
|
44
|
+
# Redis has no rollback. So a mixed outcome says how many landed: a bare error would read as
|
|
45
|
+
# "nothing changed" while some dates are already locked.
|
|
46
|
+
if failed.empty?
|
|
47
|
+
flash[:success] = t('.moved', count: moved.size)
|
|
48
|
+
else
|
|
49
|
+
reason = error_sentence(failed.first.error.value)
|
|
50
|
+
flash[:error] = t('.partially_moved', count: moved.size, total: items.size, reason: reason)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
redirect_back_to_caller
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
private
|
|
57
|
+
|
|
58
|
+
def load_parent
|
|
59
|
+
# Handled here rather than left to ResourceController's rescue_from: its handler redirects to
|
|
60
|
+
# collection_url, which for this controller resolves to a route that does not exist
|
|
61
|
+
# (model_class is InventoryItem, which has no admin resource route) — turning one 500 into
|
|
62
|
+
# another. An unknown or renamed slug belongs back at the product list.
|
|
63
|
+
@product = Spree::Product.find_by(slug: params[:product_id])
|
|
64
|
+
return if @product
|
|
65
|
+
|
|
66
|
+
flash[:error] = t('spree.admin.inventory_locks.create.product_not_found')
|
|
67
|
+
redirect_to spree.admin_products_path
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def move(inventory_item)
|
|
71
|
+
SpreeCmCommissioner::InventoryItems::MoveToLocked.call(
|
|
72
|
+
inventory_item: inventory_item,
|
|
73
|
+
quantity: params[:quantity],
|
|
74
|
+
caller_source: "#{self.class.name}#create"
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# A date range covers one inventory item per date; without one, ONLY the undated row.
|
|
79
|
+
#
|
|
80
|
+
# Never falls back to "every active date". Both callers can omit the range — the private sale's
|
|
81
|
+
# inline button always does — and a permanent-stock variant has a row per date for a year, so
|
|
82
|
+
# withholding across all of them because an operator left "To date" empty is unrecoverable:
|
|
83
|
+
# MoveToLocked's Redis half cannot be rolled back.
|
|
84
|
+
#
|
|
85
|
+
# Returns nil for a malformed range so #create can tell it apart from "found nothing".
|
|
86
|
+
def target_inventory_items
|
|
87
|
+
scope = @product.inventory_items.active.where(variant_id: params[:variant_id])
|
|
88
|
+
return scope.where(inventory_date: nil).to_a if params[:from_date].blank? || params[:to_date].blank?
|
|
89
|
+
|
|
90
|
+
dates = parsed_date_range
|
|
91
|
+
return nil if dates.nil?
|
|
92
|
+
|
|
93
|
+
scope.where(inventory_date: dates).to_a
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# nil when either date is unparseable, or when the range runs backwards — both are operator
|
|
97
|
+
# error, and an empty result would report the misleading "no stock on sale" instead.
|
|
98
|
+
def parsed_date_range
|
|
99
|
+
from = params[:from_date].to_date
|
|
100
|
+
to = params[:to_date].to_date
|
|
101
|
+
return nil if from > to
|
|
102
|
+
|
|
103
|
+
from..to
|
|
104
|
+
# Date::Error subclasses ArgumentError, so this covers both.
|
|
105
|
+
rescue ArgumentError
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def redirect_with_error(reason)
|
|
110
|
+
flash[:error] = error_sentence(reason)
|
|
111
|
+
redirect_back_to_caller
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# MoveToLocked's named failures carry their sentences; this controller's own reasons (a variant
|
|
115
|
+
# with no inventory item) are local. Falls back to the humanized symbol so a new failure name
|
|
116
|
+
# still reads as English rather than as a missing-translation string.
|
|
117
|
+
def error_sentence(reason)
|
|
118
|
+
service_message = t("spree_cm_commissioner.inventory_items.move_to_locked.#{reason}", default: reason.to_s.humanize)
|
|
119
|
+
|
|
120
|
+
t(".#{reason}", default: service_message)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Posted from the private sale's edit page for undated stock, and from this screen for
|
|
124
|
+
# permanent-stock date ranges, so return to whichever asked.
|
|
125
|
+
def redirect_back_to_caller
|
|
126
|
+
redirect_back fallback_location: admin_product_inventory_locks_path(@product)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def model_class
|
|
130
|
+
SpreeCmCommissioner::InventoryItem
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Admin
|
|
3
|
+
# Managing private sales: which products, sold to whom, until when — and, on edit, how much of
|
|
4
|
+
# each variant to withhold.
|
|
5
|
+
#
|
|
6
|
+
# New is deliberately thin: name, dates and three remote-search pickers, so opening it costs one
|
|
7
|
+
# query for roles and nothing else. Creating redirects straight to edit, which is where the
|
|
8
|
+
# allotment is actually set up now that the products are known.
|
|
9
|
+
#
|
|
10
|
+
class PrivateSalesController < Spree::Admin::ResourceController
|
|
11
|
+
# Also on update: ResourceController re-renders :edit when validation fails, and the stock
|
|
12
|
+
# partial reads these ivars — without this a rejected save is a 500 instead of a form.
|
|
13
|
+
before_action :load_stock_rows, only: %i[edit update]
|
|
14
|
+
before_action :load_assignable_roles, only: %i[new create edit update]
|
|
15
|
+
|
|
16
|
+
# GET /admin/private_sales
|
|
17
|
+
def index
|
|
18
|
+
# Off `collection`, which ResourceController already scoped with accessible_by(current_ability)
|
|
19
|
+
# — going through model_class directly would discard that.
|
|
20
|
+
@private_sales = collection.order(created_at: :desc).page(params[:page]).per(25)
|
|
21
|
+
|
|
22
|
+
load_audience_names
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# PATCH /admin/private_sales/:id/revoke
|
|
26
|
+
#
|
|
27
|
+
# The point of storing the rule rather than signing it into the URL: one sale dies without
|
|
28
|
+
# touching the lock, so every other sale drawing on the same stock keeps working.
|
|
29
|
+
def revoke
|
|
30
|
+
# @private_sale is already loaded and authorized by ResourceController#load_resource.
|
|
31
|
+
@private_sale.revoked!
|
|
32
|
+
|
|
33
|
+
flash[:success] = t('.revoked')
|
|
34
|
+
redirect_to admin_private_sales_path
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Names for every product, user and role named by the sales ON THIS PAGE, in three queries.
|
|
40
|
+
# The ids live in jsonb rather than join tables, so there is no association for `includes` to
|
|
41
|
+
# preload — asking each row for its own would be 3 queries per row.
|
|
42
|
+
def load_audience_names
|
|
43
|
+
sales = @private_sales.to_a
|
|
44
|
+
|
|
45
|
+
@product_names = Spree::Product.where(id: sales.flat_map(&:product_ids)).pluck(:id, :name).to_h
|
|
46
|
+
@user_emails = Spree::User.where(id: sales.flat_map(&:user_ids)).pluck(:id, :email).to_h
|
|
47
|
+
@role_names = Spree::Role.where(id: sales.flat_map(&:role_ids))
|
|
48
|
+
.pluck(:id, :presentation, :name)
|
|
49
|
+
.to_h { |id, presentation, name| [id, presentation.presence || name] }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Straight to edit rather than back to the list: a sale is not usable until stock is withheld
|
|
53
|
+
# for it, and edit is where that happens. Landing on the index would hide the next step.
|
|
54
|
+
# non_vendor: a private sale grants access across the store, and this repo's roles include
|
|
55
|
+
# per-vendor and per-event check-in roles whose `presentation` is only unique within a vendor —
|
|
56
|
+
# listing them all renders hundreds of indistinguishable duplicate options.
|
|
57
|
+
def load_assignable_roles
|
|
58
|
+
@assignable_roles = Spree::Role.non_vendor.order(:name)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def location_after_save
|
|
62
|
+
edit_admin_private_sale_path(@private_sale)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Everything the edit page's per-variant lock controls need, in three queries regardless of how
|
|
66
|
+
# many products the sale names.
|
|
67
|
+
#
|
|
68
|
+
# Only UNDATED inventory items are loaded. A permanent-stock product has one row per date —
|
|
69
|
+
# hundreds per variant — which no inline control can express and which would make this page as
|
|
70
|
+
# slow as the table is large. Those link out to the product's own date-range screen instead.
|
|
71
|
+
def load_stock_rows
|
|
72
|
+
product_ids = @private_sale.product_ids
|
|
73
|
+
return @variants_by_product = {} if product_ids.empty?
|
|
74
|
+
|
|
75
|
+
@products_by_id = Spree::Product.where(id: product_ids).index_by(&:id)
|
|
76
|
+
|
|
77
|
+
# Master is excluded when a product has real variants — it carries no stock of its own there
|
|
78
|
+
# and would render as a blank row. A product with no variants sells through master, so it is
|
|
79
|
+
# the fallback. Same rule as Spree::Admin::StockManagementsController.
|
|
80
|
+
# option_values: :option_type, not just :option_values — sku_and_options_text walks the second
|
|
81
|
+
# hop, so preloading one level still fires a query per option value.
|
|
82
|
+
variants = Spree::Variant.where(product_id: product_ids)
|
|
83
|
+
.includes(option_values: :option_type)
|
|
84
|
+
.order(:id)
|
|
85
|
+
by_product = variants.group_by(&:product_id)
|
|
86
|
+
|
|
87
|
+
@variants_by_product = by_product.transform_values do |list|
|
|
88
|
+
list.reject(&:is_master?).presence || list
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
@inventory_item_by_variant = SpreeCmCommissioner::InventoryItem
|
|
92
|
+
.where(variant_id: @variants_by_product.values.flatten.map(&:id), inventory_date: nil)
|
|
93
|
+
.index_by(&:variant_id)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# override — the audience is posted as id arrays by the select2 pickers in _form.
|
|
97
|
+
def permitted_resource_params
|
|
98
|
+
params.require(:private_sale)
|
|
99
|
+
.permit(:name, :expires_at, product_ids: [], role_ids: [], user_ids: [])
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
# override — created_by is ours to set, never the form's.
|
|
103
|
+
def build_resource
|
|
104
|
+
model_class.new(created_by: try_spree_current_user)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def collection_url(options = {})
|
|
108
|
+
admin_private_sales_path(options)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def model_class
|
|
112
|
+
SpreeCmCommissioner::PrivateSale
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -10,10 +10,14 @@ module Spree
|
|
|
10
10
|
CACHE_EXPIRES_IN = 1.minute
|
|
11
11
|
|
|
12
12
|
def collection
|
|
13
|
-
@collection ||=
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
@collection ||= if restricted?
|
|
14
|
+
SpreeCmCommissioner::CheckIn.none
|
|
15
|
+
else
|
|
16
|
+
SpreeCmCommissioner::CheckIn
|
|
17
|
+
.where(checkinable_type: 'Spree::Taxon', checkinable_id: params[:taxon_id])
|
|
18
|
+
.page(params[:page])
|
|
19
|
+
.per(params[:per_page])
|
|
20
|
+
end
|
|
17
21
|
end
|
|
18
22
|
|
|
19
23
|
# override
|
|
@@ -41,6 +45,20 @@ module Spree
|
|
|
41
45
|
|
|
42
46
|
private
|
|
43
47
|
|
|
48
|
+
# Non-organizers may only see check-in history for an event they're assigned
|
|
49
|
+
# to, and only while it's ongoing — once it ends, it becomes organizer-owned
|
|
50
|
+
# history (admins assign themselves the organizer role explicitly if they
|
|
51
|
+
# need access). A missing/nonexistent taxon_id is restricted the same way.
|
|
52
|
+
def restricted?
|
|
53
|
+
return false if spree_current_user.organizer?
|
|
54
|
+
|
|
55
|
+
taxon = Spree::Taxon.find_by(id: params[:taxon_id])
|
|
56
|
+
return true if taxon.nil?
|
|
57
|
+
return true unless spree_current_user.events.exists?(id: taxon.id)
|
|
58
|
+
|
|
59
|
+
taxon.ended?
|
|
60
|
+
end
|
|
61
|
+
|
|
44
62
|
def model_class
|
|
45
63
|
SpreeCmCommissioner::CheckIn
|
|
46
64
|
end
|
|
@@ -7,23 +7,31 @@ module Spree
|
|
|
7
7
|
ADDITIONAL_FIELDS = %i[participation entry_type].freeze
|
|
8
8
|
|
|
9
9
|
def index
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
10
|
+
resources = if restricted?
|
|
11
|
+
[]
|
|
12
|
+
else
|
|
13
|
+
kyc_fields = fetch_filtered_kyc_fields
|
|
14
|
+
types = ADDITIONAL_FIELDS + kyc_fields
|
|
15
|
+
|
|
16
|
+
types.map do |type|
|
|
17
|
+
# Create unique ID by using chart_type as the identifier
|
|
18
|
+
SpreeCmCommissioner::EventChart.new(
|
|
19
|
+
id: type.to_s,
|
|
20
|
+
chart_type: type,
|
|
21
|
+
product_charts: nil
|
|
22
|
+
)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
21
25
|
|
|
22
26
|
render json: SpreeCmCommissioner::V2::Operator::PieChartEventAggregatorSerializer.new(resources).serializable_hash
|
|
23
27
|
end
|
|
24
28
|
|
|
25
29
|
def resource
|
|
26
|
-
@resource =
|
|
30
|
+
@resource = if restricted?
|
|
31
|
+
SpreeCmCommissioner::EventChart.new(id: params[:id], chart_type: params[:id] || 'participation', product_charts: [])
|
|
32
|
+
else
|
|
33
|
+
event_chart_aggregator_queries.call
|
|
34
|
+
end
|
|
27
35
|
end
|
|
28
36
|
|
|
29
37
|
def resource_serializer
|
|
@@ -32,6 +40,20 @@ module Spree
|
|
|
32
40
|
|
|
33
41
|
private
|
|
34
42
|
|
|
43
|
+
# Non-organizers may only see chart data for an event they're assigned to,
|
|
44
|
+
# and only while it's ongoing — once it ends, it becomes organizer-owned
|
|
45
|
+
# history (admins assign themselves the organizer role explicitly if they
|
|
46
|
+
# need access). A missing/nonexistent taxon_id is restricted the same way.
|
|
47
|
+
def restricted?
|
|
48
|
+
return false if spree_current_user&.organizer?
|
|
49
|
+
|
|
50
|
+
taxon = Spree::Taxon.find_by(id: params[:taxon_id])
|
|
51
|
+
return true if taxon.nil?
|
|
52
|
+
return true unless spree_current_user&.events&.exists?(id: taxon.id)
|
|
53
|
+
|
|
54
|
+
taxon.ended?
|
|
55
|
+
end
|
|
56
|
+
|
|
35
57
|
def event_chart_aggregator_queries
|
|
36
58
|
SpreeCmCommissioner::EventChartQueries.new(
|
|
37
59
|
taxon_id: params[:taxon_id],
|
|
@@ -18,6 +18,13 @@ module Spree
|
|
|
18
18
|
request.headers['X-Cm-App-Name']
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
# Set by DefaultInterceptor (cm-market-app/packages/core) on every request, across every
|
|
22
|
+
# app (customer, operator, terminal) that shares it — the single source for device_id,
|
|
23
|
+
# replacing the old per-endpoint params[:device_id] (never actually sent by any client).
|
|
24
|
+
def device_id
|
|
25
|
+
request.headers['X-Cm-Device-Id']
|
|
26
|
+
end
|
|
27
|
+
|
|
21
28
|
def required_schema_validation!
|
|
22
29
|
return true if required_schema.nil?
|
|
23
30
|
|
|
@@ -10,13 +10,18 @@ module Spree
|
|
|
10
10
|
# Endpoint: POST /api/v2/storefront/draft_orders
|
|
11
11
|
# Params:
|
|
12
12
|
# - line_items: Array of { variant_id:, quantity:, options: {}, public_metadata: {}, private_metadata: {} } (required)
|
|
13
|
+
# - st: private sale token granting this order the product's locked stock (optional).
|
|
13
14
|
def create
|
|
14
15
|
result = SpreeCmCommissioner::DraftOrders::Create.call(
|
|
15
16
|
line_items: draft_order_line_items,
|
|
16
17
|
user: spree_current_user,
|
|
17
18
|
store: current_store,
|
|
18
19
|
currency: current_currency,
|
|
19
|
-
options: {
|
|
20
|
+
options: {
|
|
21
|
+
device_id: device_id,
|
|
22
|
+
last_ip_address: request.remote_ip,
|
|
23
|
+
private_sale_token: params[:st]
|
|
24
|
+
}
|
|
20
25
|
)
|
|
21
26
|
|
|
22
27
|
if result.success?
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V2
|
|
4
|
+
module Storefront
|
|
5
|
+
# GET /api/v2/storefront/private_sale_products?st=<private sale token>
|
|
6
|
+
class PrivateSaleProductsController < ::Spree::Api::V2::Storefront::ProductsController
|
|
7
|
+
before_action :require_spree_current_user
|
|
8
|
+
before_action :authorize_private_sale_token!
|
|
9
|
+
|
|
10
|
+
private
|
|
11
|
+
|
|
12
|
+
attr_reader :private_sale
|
|
13
|
+
|
|
14
|
+
def authorize_private_sale_token!
|
|
15
|
+
result = SpreeCmCommissioner::PrivateSales::Authorize.call(
|
|
16
|
+
token: params[:st],
|
|
17
|
+
user: spree_current_user
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return @private_sale = result.value if result.success?
|
|
21
|
+
|
|
22
|
+
render_error_payload(SpreeCmCommissioner::PrivateSales::Authorize.error_message(result.error.value), 403)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# override — the token names the products, so the client sends no ids. Returns them as a
|
|
26
|
+
# collection: one link is an agent's whole allotment across an event, not a single product.
|
|
27
|
+
def scope(**args)
|
|
28
|
+
super.where(id: private_sale.product_ids)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V2
|
|
4
|
+
module Storefront
|
|
5
|
+
class QueueDraftOrdersController < ::Spree::Api::V2::ResourceController
|
|
6
|
+
# Creating the order (once the job runs) acquires the inventory hold, so honor the
|
|
7
|
+
# load-test bypass here too, same as DraftOrdersController.
|
|
8
|
+
include SpreeCmCommissioner::LoadTestBypassable
|
|
9
|
+
|
|
10
|
+
# Accepts a draft order request without building it: writes a queued Firestore status
|
|
11
|
+
# document and schedules the real build (SpreeCmCommissioner::DraftOrders::Create) on a
|
|
12
|
+
# background job. The app watches the returned document for the eventual outcome
|
|
13
|
+
# instead of waiting on this request.
|
|
14
|
+
#
|
|
15
|
+
# Endpoint: POST /api/v2/storefront/queue_draft_orders
|
|
16
|
+
# Params: same as POST /api/v2/storefront/draft_orders — line_items: Array of
|
|
17
|
+
# { variant_id:, quantity:, options: {}, public_metadata: {}, private_metadata: {} } (required)
|
|
18
|
+
# st: private sale token granting this order the product's locked stock (optional).
|
|
19
|
+
def create
|
|
20
|
+
result = SpreeCmCommissioner::QueueDraftOrders::Enqueue.call(
|
|
21
|
+
create_type: 'generic',
|
|
22
|
+
creation_params: {
|
|
23
|
+
line_items: draft_order_line_items,
|
|
24
|
+
user: spree_current_user,
|
|
25
|
+
store: current_store,
|
|
26
|
+
currency: current_currency,
|
|
27
|
+
options: {
|
|
28
|
+
device_id: device_id,
|
|
29
|
+
last_ip_address: request.remote_ip,
|
|
30
|
+
private_sale_token: params[:st]
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
locale: I18n.locale.to_s
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
if result.success?
|
|
37
|
+
render_serialized_payload { result.value }
|
|
38
|
+
else
|
|
39
|
+
render_error_payload(result.error)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
# .to_h.deep_symbolize_keys converts every level (including line_items and their nested
|
|
46
|
+
# options/metadata) from ActionController::Parameters into plain Ruby Hashes with symbol
|
|
47
|
+
# keys — required because this crosses an ActiveJob serialization boundary
|
|
48
|
+
# (QueueDraftOrders::ProcessJob), unlike DraftOrdersController's version of this method,
|
|
49
|
+
# which hands Parameters straight to DraftOrders::Create within the same request.
|
|
50
|
+
def draft_order_line_items
|
|
51
|
+
params.permit(line_items: [:variant_id, :quantity, { options: {}, public_metadata: {}, private_metadata: {} }])
|
|
52
|
+
.to_h
|
|
53
|
+
.deep_symbolize_keys
|
|
54
|
+
.fetch(:line_items, [])
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -15,7 +15,10 @@ module Spree
|
|
|
15
15
|
remote_ip: request.remote_ip,
|
|
16
16
|
waiting_guest_firebase_doc_id: params[:waiting_guest_firebase_doc_id],
|
|
17
17
|
page_path: params[:page_path],
|
|
18
|
-
device_attributes: permitted_device_attributes
|
|
18
|
+
device_attributes: permitted_device_attributes,
|
|
19
|
+
bypass_jwt: params[:bypass_jwt],
|
|
20
|
+
bypass_order_token: request.headers['X-Spree-Order-Token'],
|
|
21
|
+
user: spree_current_user
|
|
19
22
|
)
|
|
20
23
|
|
|
21
24
|
if context.success?
|
|
@@ -7,6 +7,7 @@ module Spree
|
|
|
7
7
|
# Endpoint: POST /api/v2/tenant/draft_orders
|
|
8
8
|
# Params:
|
|
9
9
|
# - line_items: Array of { variant_id:, quantity:, options: {}, public_metadata: {}, private_metadata: {} } (required)
|
|
10
|
+
# - st: private sale token granting this order the product's locked stock (optional).
|
|
10
11
|
def create
|
|
11
12
|
result = SpreeCmCommissioner::DraftOrders::Create.call(
|
|
12
13
|
line_items: draft_order_line_items,
|
|
@@ -14,7 +15,11 @@ module Spree
|
|
|
14
15
|
store: current_store,
|
|
15
16
|
currency: current_currency,
|
|
16
17
|
tenant: @tenant,
|
|
17
|
-
options: {
|
|
18
|
+
options: {
|
|
19
|
+
device_id: params[:device_id],
|
|
20
|
+
last_ip_address: request.remote_ip,
|
|
21
|
+
private_sale_token: params[:st]
|
|
22
|
+
}
|
|
18
23
|
)
|
|
19
24
|
|
|
20
25
|
if result.success?
|
|
@@ -8,12 +8,23 @@ module SpreeCmCommissioner
|
|
|
8
8
|
SpreeCmCommissioner::OrderGeneralNotification.with(
|
|
9
9
|
notificable: order,
|
|
10
10
|
title: title,
|
|
11
|
-
message: message(order)
|
|
11
|
+
message: message(order),
|
|
12
|
+
payload: payload(order)
|
|
12
13
|
).deliver_later(order.user)
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
private
|
|
16
17
|
|
|
18
|
+
# Overrides notification_type so the app can route this tap to the payment screen instead of
|
|
19
|
+
# the booking details every other OrderGeneralNotification lands on. The screen loads the cart
|
|
20
|
+
# by token, not by number, so the token has to ride along or the tap has nothing to open.
|
|
21
|
+
def payload(order)
|
|
22
|
+
{
|
|
23
|
+
notification_type: 'payment_incomplete_notification',
|
|
24
|
+
order_token: order.token
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
17
28
|
def title
|
|
18
29
|
I18n.t('notifications.spree_cm_commissioner.payment_incomplete_notification.title')
|
|
19
30
|
end
|