spree_cm_commissioner 2.14.7 → 2.14.9
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/controllers/concerns/spree_cm_commissioner/content_cachable.rb +9 -1
- data/app/controllers/concerns/spree_cm_commissioner/guest_owner_authorization.rb +30 -0
- data/app/controllers/spree/admin/stock_managements_controller.rb +12 -1
- data/app/controllers/spree/api/v2/organizer/invites_controller.rb +5 -1
- data/app/controllers/spree/api/v2/storefront/guests_controller.rb +13 -7
- data/app/controllers/spree/api/v2/storefront/id_cards_controller.rb +6 -4
- data/app/controllers/spree/api/v2/storefront/products_controller_decorator.rb +1 -1
- data/app/controllers/spree/api/v2/storefront/s3_signed_urls_controller.rb +4 -3
- data/app/controllers/spree/api/v2/tenant/base_controller.rb +23 -3
- data/app/controllers/spree/api/v2/tenant/cart_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/id_cards_controller.rb +6 -4
- data/app/controllers/spree/api/v2/tenant/payment_methods_controller.rb +1 -2
- data/app/controllers/spree/api/v2/tenant/products_controller.rb +1 -1
- data/app/controllers/spree/api/v2/tenant/s3_signed_urls_controller.rb +4 -3
- data/app/interactors/spree_cm_commissioner/guest_id_card_manager.rb +18 -9
- data/app/interactors/spree_cm_commissioner/id_card_image_updater.rb +6 -3
- data/app/interactors/spree_cm_commissioner/image_saver.rb +4 -3
- data/app/interactors/spree_cm_commissioner/profile_image_updater.rb +6 -3
- data/app/jobs/concerns/spree_cm_commissioner/idempotent_job.rb +7 -6
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_job.rb +4 -13
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert_job.rb +2 -21
- data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_hold_job.rb +3 -29
- data/app/models/concerns/spree_cm_commissioner/firestore_connection.rb +7 -9
- data/app/models/concerns/spree_cm_commissioner/order_state_machine.rb +1 -9
- data/app/models/concerns/spree_cm_commissioner/sales_channel_bitwise.rb +44 -0
- data/app/models/concerns/spree_cm_commissioner/user_uploaded_image.rb +17 -0
- data/app/models/spree_cm_commissioner/back_image.rb +2 -0
- data/app/models/spree_cm_commissioner/front_image.rb +2 -0
- data/app/models/spree_cm_commissioner/inventory_hold.rb +5 -0
- data/app/models/spree_cm_commissioner/order_decorator.rb +11 -0
- data/app/models/spree_cm_commissioner/product_decorator.rb +4 -0
- data/app/models/spree_cm_commissioner/s3_presigned_url.rb +8 -7
- data/app/models/spree_cm_commissioner/s3_presigned_url_builder.rb +9 -3
- data/app/models/spree_cm_commissioner/tenant.rb +3 -0
- data/app/models/spree_cm_commissioner/user_profile.rb +2 -0
- data/app/overrides/spree/admin/products/_form/enable_confirmation_email.html.erb.deface +13 -0
- data/app/overrides/spree/admin/products/_form/sellable_on_platform.html.erb.deface +12 -0
- data/app/serializers/spree/v2/storefront/taxon_serializer_decorator.rb +4 -1
- data/app/services/spree_cm_commissioner/api_caches/invalidate.rb +28 -17
- data/app/services/spree_cm_commissioner/api_caches/invalidate_all.rb +5 -7
- data/app/services/spree_cm_commissioner/draft_orders/create.rb +6 -3
- data/app/services/spree_cm_commissioner/events/publish_stock_statuses_to_firestore.rb +30 -4
- data/app/services/spree_cm_commissioner/inventory_holds/acquire.rb +28 -17
- data/app/services/spree_cm_commissioner/inventory_holds/convert.rb +1 -0
- data/app/services/spree_cm_commissioner/inventory_holds/release.rb +1 -0
- data/app/services/spree_cm_commissioner/queue_draft_orders/publish_status.rb +8 -8
- data/app/services/spree_cm_commissioner/redis_stock/base.rb +7 -1
- data/app/services/spree_cm_commissioner/waiting_room/publish_lobby_path.rb +1 -8
- data/app/services/spree_cm_commissioner/waiting_room_system_metadata_fetcher.rb +2 -8
- data/config/initializers/firestore.rb +19 -0
- data/config/initializers/spree_permitted_attributes.rb +4 -0
- data/config/locales/en.yml +2 -0
- data/config/locales/km.yml +1 -0
- data/config/routes.rb +14 -6
- data/db/migrate/20261103000001_add_sales_channel_to_spree_products.rb +14 -0
- data/lib/spree_cm_commissioner/firestore_clients.rb +49 -0
- data/lib/spree_cm_commissioner/s3_url_generator.rb +13 -1
- data/lib/spree_cm_commissioner/tenant_slug_constraint.rb +23 -0
- data/lib/spree_cm_commissioner/version.rb +1 -1
- data/lib/spree_cm_commissioner.rb +2 -0
- metadata +11 -9
- data/app/controllers/spree/api/v2/organizer/images_controller.rb +0 -56
- data/app/controllers/spree/api/v2/organizer/s3_signed_urls_controller.rb +0 -29
- data/app/controllers/spree/api/v2/organizer/ticket_images_controller.rb +0 -27
- data/app/controllers/spree/api/v2/organizer/tickets_controller.rb +0 -97
- data/app/serializers/spree/v2/organizer/image_serializer.rb +0 -11
- data/app/serializers/spree/v2/organizer/s3_signed_url_serializer.rb +0 -10
- data/app/serializers/spree/v2/organizer/ticket_serializer.rb +0 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b5426f6a834611d1c303d1305de10998cea1cd5b6ca72c23f512ddff53614b2a
|
|
4
|
+
data.tar.gz: cb6688095f5daef9df50906c20d5adb5cba20dc4f2657a430b1d2ab00afaaaf6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e609a94743b1283a225d077dad5ba3f775ed599985d687f8b6c4408de286bc6f8b6e91e4024e675ec03ae06a67a013fa43cc6242cc50070a334bc2ac4731e7d0
|
|
7
|
+
data.tar.gz: d007f4f9a802452c020d32f32f44c466e88469ea24386c87f8b45c834ced740d1faf55185d953901640fd1dd73f19af686667ef9f29e615b97642aadc8645020
|
data/Gemfile.lock
CHANGED
|
@@ -88,8 +88,16 @@ module SpreeCmCommissioner
|
|
|
88
88
|
ContentCachable.cdn_max_age(cdn_cache_config&.dig(:max_age))
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
# A tenant API is cacheable only on its /api/v2/:tenant_slug/ path. The legacy /api/v2/tenant/
|
|
92
|
+
# path takes the tenant from the OAuth token, which is not in the cache key, so caching it would
|
|
93
|
+
# serve one tenant's response to every other tenant.
|
|
91
94
|
def cdn_cacheable?
|
|
92
|
-
request.get? && cdn_cache_config.present? && cdn_cache_config[:actions].include?(action_name)
|
|
95
|
+
request.get? && cdn_cache_config.present? && cdn_cache_config[:actions].include?(action_name) &&
|
|
96
|
+
(!tenant_api? || params[:tenant_slug].present?)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def tenant_api?
|
|
100
|
+
self.class.name.to_s.start_with?('Spree::Api::V2::Tenant::')
|
|
93
101
|
end
|
|
94
102
|
|
|
95
103
|
def set_cache_control_for_cdn
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
# For per-guest endpoints that touch the guest's ID card photos. The app calls these during
|
|
3
|
+
# checkout (order token, or the logged-in owner) and after it from pending guests (logged-in
|
|
4
|
+
# owner of a complete order). That's why ownership is checked here and not with the
|
|
5
|
+
# `:update, Order` ability, which refuses completed orders.
|
|
6
|
+
module GuestOwnerAuthorization
|
|
7
|
+
extend ActiveSupport::Concern
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
# 404 rather than 403 for someone else's guest, so sequential guest ids can't be probed.
|
|
12
|
+
def authorized_guest
|
|
13
|
+
@authorized_guest ||= begin
|
|
14
|
+
guest = SpreeCmCommissioner::Guest.find(params[:guest_id])
|
|
15
|
+
raise ActiveRecord::RecordNotFound unless guest_owned_by_requester?(guest)
|
|
16
|
+
|
|
17
|
+
guest
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def guest_owned_by_requester?(guest)
|
|
22
|
+
order = guest.line_item&.order
|
|
23
|
+
user = spree_current_user
|
|
24
|
+
return true if user.present? && (guest.user_id == user.id || order&.user_id == user.id)
|
|
25
|
+
|
|
26
|
+
token = request.headers['X-Spree-Order-Token'].presence || params[:order_token].presence
|
|
27
|
+
token.present? && order&.token.present? && ActiveSupport::SecurityUtils.secure_compare(order.token, token)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -1,14 +1,25 @@
|
|
|
1
1
|
module Spree
|
|
2
2
|
module Admin
|
|
3
3
|
class StockManagementsController < Spree::Admin::ResourceController
|
|
4
|
+
include Spree::Admin::ProductConcern
|
|
5
|
+
|
|
4
6
|
skip_before_action :load_resource
|
|
5
7
|
|
|
6
8
|
before_action :load_parent
|
|
7
9
|
|
|
8
10
|
helper SpreeCmCommissioner::Admin::InventoryItemsHelper
|
|
9
11
|
|
|
12
|
+
# Same lookup as Spree's ProductsController: friendly.find accepts both slug and id links
|
|
13
|
+
# (e.g. /admin/products/938/stock_managements) and raises on a miss instead of leaving @product nil.
|
|
10
14
|
def load_parent
|
|
11
|
-
@product =
|
|
15
|
+
@product = product_scope.friendly.find(params[:product_id])
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# ResourceController's handler names model_class (StockItem) in the flash and redirects to
|
|
19
|
+
# collection_url (/admin/stock_items), which has no GET route — so point both at products instead.
|
|
20
|
+
def resource_not_found
|
|
21
|
+
flash[:error] = flash_message_for(Spree::Product.new, :not_found)
|
|
22
|
+
redirect_to spree.admin_products_path
|
|
12
23
|
end
|
|
13
24
|
|
|
14
25
|
def index
|
|
@@ -3,6 +3,10 @@ module Spree
|
|
|
3
3
|
module V2
|
|
4
4
|
module Organizer
|
|
5
5
|
class InvitesController < ::Spree::Api::V2::Organizer::BaseController
|
|
6
|
+
# Accepting grants the operator role, so it must be the invitee's own decision: the
|
|
7
|
+
# token alone only proves someone has the link.
|
|
8
|
+
before_action :require_spree_current_user, only: :update
|
|
9
|
+
|
|
6
10
|
def show
|
|
7
11
|
if invite&.url_valid?
|
|
8
12
|
render_serialized_payload { serialize_resource(invite) }
|
|
@@ -12,7 +16,7 @@ module Spree
|
|
|
12
16
|
end
|
|
13
17
|
|
|
14
18
|
def update
|
|
15
|
-
context = invite.confirm(
|
|
19
|
+
context = invite.confirm(spree_current_user.id)
|
|
16
20
|
|
|
17
21
|
if context.success?
|
|
18
22
|
render_serialized_payload { serialize_resource(context.invite) }
|
|
@@ -4,6 +4,7 @@ module Spree
|
|
|
4
4
|
module Storefront
|
|
5
5
|
class GuestsController < ::Spree::Api::V2::ResourceController
|
|
6
6
|
include OrderConcern
|
|
7
|
+
include SpreeCmCommissioner::GuestOwnerAuthorization
|
|
7
8
|
|
|
8
9
|
# raise 404 when no order, except for show
|
|
9
10
|
before_action :ensure_order, except: [:show]
|
|
@@ -23,9 +24,9 @@ module Spree
|
|
|
23
24
|
SpreeCmCommissioner::V2::Storefront::GuestSerializer
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
# The serializer includes the guest's ID card, so show must be as strict as id_cards.
|
|
26
28
|
def show
|
|
27
|
-
|
|
28
|
-
render_serialized_payload { serialize_resource(@guest) }
|
|
29
|
+
render_serialized_payload { serialize_resource(owned_guest) }
|
|
29
30
|
end
|
|
30
31
|
|
|
31
32
|
def create
|
|
@@ -44,9 +45,7 @@ module Spree
|
|
|
44
45
|
end
|
|
45
46
|
|
|
46
47
|
def update
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
result = SpreeCmCommissioner::Guests::Update.call(guest: guest_for_update, guest_params: guest_params)
|
|
48
|
+
result = SpreeCmCommissioner::Guests::Update.call(guest: owned_guest, guest_params: guest_params)
|
|
50
49
|
|
|
51
50
|
if result.success?
|
|
52
51
|
render_serialized_payload { serialize_resource(result.value[:guest]) }
|
|
@@ -66,8 +65,15 @@ module Spree
|
|
|
66
65
|
@parent ||= spree_current_order.line_items.find(params[:line_item_id])
|
|
67
66
|
end
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
|
|
68
|
+
# Checked against the guest's own order: spree_current_order is just the caller's cart,
|
|
69
|
+
# which says nothing about whether they own this guest.
|
|
70
|
+
def owned_guest
|
|
71
|
+
@owned_guest ||= begin
|
|
72
|
+
guest = SpreeCmCommissioner::Guest.find(params[:id])
|
|
73
|
+
raise ActiveRecord::RecordNotFound unless guest_owned_by_requester?(guest)
|
|
74
|
+
|
|
75
|
+
guest
|
|
76
|
+
end
|
|
71
77
|
end
|
|
72
78
|
|
|
73
79
|
def guest_params
|
|
@@ -3,12 +3,14 @@ module Spree
|
|
|
3
3
|
module V2
|
|
4
4
|
module Storefront
|
|
5
5
|
class IdCardsController < ::Spree::Api::V2::ResourceController
|
|
6
|
+
include SpreeCmCommissioner::GuestOwnerAuthorization
|
|
7
|
+
|
|
6
8
|
def create
|
|
7
9
|
context = SpreeCmCommissioner::GuestIdCardManager.call(
|
|
8
10
|
card_type: id_card_params[:card_type],
|
|
9
11
|
front_image_url: id_card_params[:front_image_url],
|
|
10
12
|
back_image_url: id_card_params[:back_image_url],
|
|
11
|
-
|
|
13
|
+
guest: authorized_guest
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
if context.success?
|
|
@@ -23,7 +25,7 @@ module Spree
|
|
|
23
25
|
card_type: id_card_params[:card_type],
|
|
24
26
|
front_image_url: id_card_params[:front_image_url],
|
|
25
27
|
back_image_url: id_card_params[:back_image_url],
|
|
26
|
-
|
|
28
|
+
guest: authorized_guest
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
if context.success?
|
|
@@ -34,7 +36,7 @@ module Spree
|
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def destroy
|
|
37
|
-
id_card = SpreeCmCommissioner::IdCard.
|
|
39
|
+
id_card = SpreeCmCommissioner::IdCard.find_by!(id: params[:id], guest_id: authorized_guest.id)
|
|
38
40
|
|
|
39
41
|
if id_card.destroy
|
|
40
42
|
render_serialized_payload(204) { serialize_resource(id_card) }
|
|
@@ -46,7 +48,7 @@ module Spree
|
|
|
46
48
|
private
|
|
47
49
|
|
|
48
50
|
def id_card_params
|
|
49
|
-
params.permit(:card_type, :front_image_url, :back_image_url
|
|
51
|
+
params.permit(:card_type, :front_image_url, :back_image_url)
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def model_class
|
|
@@ -12,10 +12,11 @@ module Spree
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def resource
|
|
15
|
+
# Never take bucket_name/object_key from params: the key is always generated server-side.
|
|
16
|
+
# Reachable without login (app token only), so S3 also enforces the upload size.
|
|
15
17
|
SpreeCmCommissioner::S3PresignedUrlBuilder.call(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
file_name: params[:file_name]
|
|
18
|
+
file_name: params[:file_name],
|
|
19
|
+
content_length_range: 1..SpreeCmCommissioner::UserUploadedImage::MAX_SIZE
|
|
19
20
|
)
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -8,14 +8,34 @@ module Spree
|
|
|
8
8
|
set_current_tenant_through_filter
|
|
9
9
|
before_action :require_tenant
|
|
10
10
|
|
|
11
|
+
# /api/v2/:tenant_slug/... takes the tenant from the URL so the CDN, which caches by URL,
|
|
12
|
+
# never serves one tenant's response to another. Legacy /api/v2/tenant/... still takes it
|
|
13
|
+
# from the OAuth token and is not CDN-cached (see SpreeCmCommissioner::ContentCachable).
|
|
11
14
|
def require_tenant
|
|
12
|
-
|
|
13
|
-
raise Doorkeeper::Errors::DoorkeeperError if
|
|
15
|
+
@tenant = tenant_from_path || tenant_from_token
|
|
16
|
+
raise Doorkeeper::Errors::DoorkeeperError if @tenant.nil?
|
|
14
17
|
|
|
15
|
-
@tenant = doorkeeper_token.application.tenant
|
|
16
18
|
set_current_tenant(@tenant)
|
|
17
19
|
end
|
|
18
20
|
|
|
21
|
+
# Slugs only (current or FriendlyId history, so a renamed tenant keeps working in shipped
|
|
22
|
+
# apps), never numeric ids, so each tenant has one cacheable URL. A token minted for another
|
|
23
|
+
# tenant's app, or for the BookMe+ storefront app, is refused rather than letting its user
|
|
24
|
+
# act inside this tenant.
|
|
25
|
+
def tenant_from_path
|
|
26
|
+
return if params[:tenant_slug].blank?
|
|
27
|
+
|
|
28
|
+
# FriendlyId's own finder (slug + history, never the numeric id), not a dynamic finder.
|
|
29
|
+
tenant = SpreeCmCommissioner::Tenant.friendly.find_by_friendly_id(params[:tenant_slug]) # rubocop:disable Rails/DynamicFindBy
|
|
30
|
+
raise Doorkeeper::Errors::DoorkeeperError if doorkeeper_token && doorkeeper_token.application&.tenant_id != tenant.id
|
|
31
|
+
|
|
32
|
+
tenant
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def tenant_from_token
|
|
36
|
+
doorkeeper_token&.application&.tenant
|
|
37
|
+
end
|
|
38
|
+
|
|
19
39
|
def current_vendor
|
|
20
40
|
@current_vendor ||= @tenant.active_vendor
|
|
21
41
|
end
|
|
@@ -192,7 +192,7 @@ module Spree
|
|
|
192
192
|
end
|
|
193
193
|
|
|
194
194
|
def load_variant
|
|
195
|
-
@variant = current_store.variants.find(add_item_params[:variant_id])
|
|
195
|
+
@variant = current_store.variants.joins(:product).merge(Spree::Product.sellable_on(:direct)).find(add_item_params[:variant_id])
|
|
196
196
|
end
|
|
197
197
|
|
|
198
198
|
def render_error_item_quantity
|
|
@@ -3,12 +3,14 @@ module Spree
|
|
|
3
3
|
module V2
|
|
4
4
|
module Tenant
|
|
5
5
|
class IdCardsController < BaseController
|
|
6
|
+
include SpreeCmCommissioner::GuestOwnerAuthorization
|
|
7
|
+
|
|
6
8
|
def create
|
|
7
9
|
context = SpreeCmCommissioner::GuestIdCardManager.call(
|
|
8
10
|
card_type: id_card_params[:card_type],
|
|
9
11
|
front_image_url: id_card_params[:front_image_url],
|
|
10
12
|
back_image_url: id_card_params[:back_image_url],
|
|
11
|
-
|
|
13
|
+
guest: authorized_guest
|
|
12
14
|
)
|
|
13
15
|
|
|
14
16
|
if context.success?
|
|
@@ -23,7 +25,7 @@ module Spree
|
|
|
23
25
|
card_type: id_card_params[:card_type],
|
|
24
26
|
front_image_url: id_card_params[:front_image_url],
|
|
25
27
|
back_image_url: id_card_params[:back_image_url],
|
|
26
|
-
|
|
28
|
+
guest: authorized_guest
|
|
27
29
|
)
|
|
28
30
|
|
|
29
31
|
if context.success?
|
|
@@ -34,7 +36,7 @@ module Spree
|
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def destroy
|
|
37
|
-
id_card = SpreeCmCommissioner::IdCard.
|
|
39
|
+
id_card = SpreeCmCommissioner::IdCard.find_by!(id: params[:id], guest_id: authorized_guest.id)
|
|
38
40
|
|
|
39
41
|
if id_card.destroy
|
|
40
42
|
head :no_content
|
|
@@ -46,7 +48,7 @@ module Spree
|
|
|
46
48
|
private
|
|
47
49
|
|
|
48
50
|
def id_card_params
|
|
49
|
-
params.permit(:card_type, :front_image_url, :back_image_url
|
|
51
|
+
params.permit(:card_type, :front_image_url, :back_image_url)
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
def model_class
|
|
@@ -7,8 +7,7 @@ module Spree
|
|
|
7
7
|
# vendor_id is a required query param, and payout is an optional one (caller
|
|
8
8
|
# passes Order#required_payway_payout? — see PaymentMethods::Find). A vendor id
|
|
9
9
|
# is globally unique and belongs to exactly one tenant, so it is what identifies
|
|
10
|
-
#
|
|
11
|
-
# token, not the URL.
|
|
10
|
+
# vendor and store here; the tenant comes from BaseController#require_tenant.
|
|
12
11
|
#
|
|
13
12
|
# IMPORTANT for caching (see SpreeCmCommissioner::ContentCachable): the CDN
|
|
14
13
|
# must include vendor_id in its cache key. If query strings are stripped,
|
|
@@ -12,10 +12,11 @@ module Spree
|
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def resource
|
|
15
|
+
# Never take bucket_name/object_key from params: the key is always generated server-side.
|
|
16
|
+
# Reachable without login (app token only), so S3 also enforces the upload size.
|
|
15
17
|
SpreeCmCommissioner::S3PresignedUrlBuilder.call(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
file_name: params[:file_name]
|
|
18
|
+
file_name: params[:file_name],
|
|
19
|
+
content_length_range: 1..SpreeCmCommissioner::UserUploadedImage::MAX_SIZE
|
|
19
20
|
)
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
class GuestIdCardManager < BaseInteractor
|
|
3
|
-
|
|
3
|
+
# guest: the caller must already have checked the requester owns this guest.
|
|
4
|
+
delegate :card_type, :front_image_url, :back_image_url, :guest, to: :context
|
|
4
5
|
|
|
5
6
|
def call
|
|
6
7
|
if guest.id_card.blank? && front_image_url.present?
|
|
@@ -31,31 +32,39 @@ module SpreeCmCommissioner
|
|
|
31
32
|
context.result = guest.id_card
|
|
32
33
|
end
|
|
33
34
|
|
|
34
|
-
def guest
|
|
35
|
-
SpreeCmCommissioner::Guest.find(guest_id)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
35
|
def manage_card_type(id_card)
|
|
39
36
|
id_card.update(card_type: card_type)
|
|
40
37
|
end
|
|
41
38
|
|
|
39
|
+
# Fails this interactor's own context: failing the updater's context instead raises a Failure
|
|
40
|
+
# that Interactor#run swallows, so the request would report success with nothing attached.
|
|
42
41
|
def manage_front_image(id_card)
|
|
43
|
-
|
|
42
|
+
return if unchanged_image?(id_card.front_image, front_image_url)
|
|
43
|
+
|
|
44
|
+
result = SpreeCmCommissioner::IdCardImageUpdater.call(
|
|
44
45
|
id_card: id_card,
|
|
45
46
|
image_url: front_image_url,
|
|
46
47
|
type: 'front_image'
|
|
47
48
|
)
|
|
48
49
|
|
|
49
|
-
context.fail!(message: 'Failed to update front image') unless
|
|
50
|
+
context.fail!(message: 'Failed to update front image') unless result.success?
|
|
50
51
|
end
|
|
51
52
|
|
|
52
53
|
def manage_back_image(id_card)
|
|
53
|
-
|
|
54
|
+
return if unchanged_image?(id_card.back_image, back_image_url)
|
|
55
|
+
|
|
56
|
+
result = SpreeCmCommissioner::IdCardImageUpdater.call(
|
|
54
57
|
id_card: id_card,
|
|
55
58
|
image_url: back_image_url,
|
|
56
59
|
type: 'back_image'
|
|
57
60
|
)
|
|
58
|
-
context.fail!(message: 'Failed to update back image') unless
|
|
61
|
+
context.fail!(message: 'Failed to update back image') unless result.success?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# The app sends the whole card back on every edit, so a side the user didn't replace arrives as
|
|
65
|
+
# that image's existing URL rather than a new upload. Keep it instead of failing the fetch.
|
|
66
|
+
def unchanged_image?(image, url)
|
|
67
|
+
image.present? && SpreeCmCommissioner::S3UrlGenerator.upload_key(url).nil?
|
|
59
68
|
end
|
|
60
69
|
end
|
|
61
70
|
end
|
|
@@ -4,10 +4,9 @@ module SpreeCmCommissioner
|
|
|
4
4
|
|
|
5
5
|
def call
|
|
6
6
|
presigned_url = SpreeCmCommissioner::S3UrlGenerator.s3_presigned_url(context.image_url)
|
|
7
|
+
response = Faraday.get(presigned_url) if presigned_url
|
|
7
8
|
|
|
8
|
-
response
|
|
9
|
-
|
|
10
|
-
if response.success?
|
|
9
|
+
if response&.success?
|
|
11
10
|
id_card = context.id_card
|
|
12
11
|
io = StringIO.new(response.body)
|
|
13
12
|
filename = File.basename(context.image_url)
|
|
@@ -20,7 +19,11 @@ module SpreeCmCommissioner
|
|
|
20
19
|
else
|
|
21
20
|
context.fail!(message: id_card_image.errors.full_messages.join(','))
|
|
22
21
|
end
|
|
22
|
+
else
|
|
23
|
+
context.fail!(message: I18n.t('s3.image.upload.fail'))
|
|
23
24
|
end
|
|
25
|
+
rescue Interactor::Failure
|
|
26
|
+
raise
|
|
24
27
|
rescue StandardError => e
|
|
25
28
|
context.fail!(message: "Error fetching the remote image: #{e.message}")
|
|
26
29
|
end
|
|
@@ -2,10 +2,9 @@ module SpreeCmCommissioner
|
|
|
2
2
|
class ImageSaver < BaseInteractor
|
|
3
3
|
def call
|
|
4
4
|
presigned_url = SpreeCmCommissioner::S3UrlGenerator.s3_presigned_url(context.url)
|
|
5
|
+
response = Faraday.get(presigned_url) if presigned_url
|
|
5
6
|
|
|
6
|
-
response
|
|
7
|
-
|
|
8
|
-
if response.success?
|
|
7
|
+
if response&.success?
|
|
9
8
|
io = StringIO.new(response.body)
|
|
10
9
|
filename = File.basename(context.url)
|
|
11
10
|
|
|
@@ -20,6 +19,8 @@ module SpreeCmCommissioner
|
|
|
20
19
|
else
|
|
21
20
|
context.fail!(message: I18n.t('s3.image.upload.fail'))
|
|
22
21
|
end
|
|
22
|
+
rescue Interactor::Failure
|
|
23
|
+
raise
|
|
23
24
|
rescue StandardError => e
|
|
24
25
|
context.fail!(message: e.message)
|
|
25
26
|
end
|
|
@@ -3,10 +3,9 @@ module SpreeCmCommissioner
|
|
|
3
3
|
# url: 'https://mybucket.s3.ap-southeast-1.amazonaws.com/uploads/5f31/1697038252270.jpg'
|
|
4
4
|
def call
|
|
5
5
|
presigned_url = SpreeCmCommissioner::S3UrlGenerator.s3_presigned_url(context.url)
|
|
6
|
+
response = Faraday.get(presigned_url) if presigned_url
|
|
6
7
|
|
|
7
|
-
response
|
|
8
|
-
|
|
9
|
-
if response.success?
|
|
8
|
+
if response&.success?
|
|
10
9
|
user = context.user
|
|
11
10
|
io = StringIO.new(response.body)
|
|
12
11
|
filename = File.basename(context.url)
|
|
@@ -20,7 +19,11 @@ module SpreeCmCommissioner
|
|
|
20
19
|
else
|
|
21
20
|
context.fail!(message: profile.errors.full_messages.join(','))
|
|
22
21
|
end
|
|
22
|
+
else
|
|
23
|
+
context.fail!(message: I18n.t('s3.image.upload.fail'))
|
|
23
24
|
end
|
|
25
|
+
rescue Interactor::Failure
|
|
26
|
+
raise
|
|
24
27
|
rescue StandardError => e
|
|
25
28
|
context.fail!(message: "Error fetching the remote image: #{e.message}")
|
|
26
29
|
end
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
module SpreeCmCommissioner
|
|
2
2
|
module IdempotentJob
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
# The enqueuer mints one key per side effect being mirrored (e.g. one per Redis change) and
|
|
4
|
+
# passes it in the job args, so a retry or re-delivery of that job dedups while a genuinely new
|
|
5
|
+
# change always applies. No fallback: a key derived from the payload made legitimate repeats
|
|
6
|
+
# (unstock after restock, a re-acquired hold) look like duplicates and silently skipped them.
|
|
7
|
+
def with_idempotency(key, &block)
|
|
8
|
+
raise ArgumentError, "#{self.class.name} requires an idempotency_key" if key.blank?
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
def default_idempotency_key
|
|
9
|
-
"#{self.class.name}:#{job_id}"
|
|
10
|
+
SpreeCmCommissioner::IdempotencyKey.run_once(key, &block)
|
|
10
11
|
end
|
|
11
12
|
end
|
|
12
13
|
end
|
|
@@ -6,26 +6,17 @@ module SpreeCmCommissioner
|
|
|
6
6
|
queue_as :inventory_item
|
|
7
7
|
|
|
8
8
|
def perform(options = {})
|
|
9
|
-
with_idempotency(
|
|
9
|
+
with_idempotency(options[:idempotency_key]) do
|
|
10
10
|
bulk_adjust_quantities!(options)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# line items no-ops. An explicit key wins; with no line items we fall back to
|
|
18
|
-
# the per-enqueue job_id (returning nil lets with_idempotency apply that default).
|
|
19
|
-
def idempotency_key_for(options)
|
|
20
|
-
return options[:idempotency_key] if options[:idempotency_key].present?
|
|
21
|
-
return nil if options[:line_item_ids].blank?
|
|
22
|
-
|
|
23
|
-
"line_item:#{options[:line_item_ids].compact.uniq.sort.join(':')}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# :line_item_ids, :inventory_id_and_quantities, :caller_source
|
|
16
|
+
# :line_item_ids, :inventory_id_and_quantities, :caller_source, :idempotency_key
|
|
27
17
|
#
|
|
28
|
-
# :caller_source is a string like "ClassName#method"
|
|
18
|
+
# :line_item_ids is for tracing only. :caller_source is a string like "ClassName#method"
|
|
19
|
+
# identifying who enqueued the job.
|
|
29
20
|
def bulk_adjust_quantities!(options = {})
|
|
30
21
|
SpreeCmCommissioner::InventoryItems::BulkAdjustQuantities.call!(
|
|
31
22
|
inventory_id_and_quantities: options[:inventory_id_and_quantities],
|
data/app/jobs/spree_cm_commissioner/inventory_items/bulk_adjust_quantities_on_convert_job.rb
CHANGED
|
@@ -6,33 +6,14 @@ module SpreeCmCommissioner
|
|
|
6
6
|
queue_as :inventory_item
|
|
7
7
|
|
|
8
8
|
def perform(options = {})
|
|
9
|
-
with_idempotency(
|
|
9
|
+
with_idempotency(options[:idempotency_key]) do
|
|
10
10
|
bulk_adjust_quantities_on_convert(options)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# An explicit key wins; with no order_id we fall back to the per-enqueue job_id (returning
|
|
18
|
-
# nil lets with_idempotency apply that default).
|
|
19
|
-
def idempotency_key_for(options)
|
|
20
|
-
return options[:idempotency_key] if options[:idempotency_key].present?
|
|
21
|
-
return nil if options[:order_id].blank?
|
|
22
|
-
|
|
23
|
-
"order:#{options[:order_id]}:convert:#{adjustments_fingerprint(options[:inventory_id_and_quantities])}"
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Readable, order-independent fingerprint of the adjustment payload, e.g. "1=2,2=-1:locked".
|
|
27
|
-
def adjustments_fingerprint(adjustments)
|
|
28
|
-
Array(adjustments)
|
|
29
|
-
.map { |adj| adj.with_indifferent_access.values_at(:inventory_id, :quantity, :locked) }
|
|
30
|
-
.sort_by { |inventory_id, quantity, locked| [inventory_id.to_s, quantity.to_s, locked.to_s] }
|
|
31
|
-
.map { |inventory_id, quantity, locked| locked ? "#{inventory_id}=#{quantity}:locked" : "#{inventory_id}=#{quantity}" }
|
|
32
|
-
.join(',')
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# :order_id, :inventory_id_and_quantities, :caller_source
|
|
16
|
+
# :order_id, :inventory_id_and_quantities, :caller_source, :idempotency_key
|
|
36
17
|
def bulk_adjust_quantities_on_convert(options = {})
|
|
37
18
|
raise ArgumentError, 'order_id is required' if options[:order_id].blank?
|
|
38
19
|
raise ArgumentError, 'inventory_id_and_quantities is required' if options[:inventory_id_and_quantities].blank?
|
|
@@ -6,42 +6,16 @@ module SpreeCmCommissioner
|
|
|
6
6
|
queue_as :inventory_item
|
|
7
7
|
|
|
8
8
|
def perform(options = {})
|
|
9
|
-
with_idempotency(
|
|
9
|
+
with_idempotency(options[:idempotency_key]) do
|
|
10
10
|
bulk_adjust_quantities_on_hold(options)
|
|
11
11
|
end
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
# on-hold payload no-ops while a genuinely different one re-applies. An explicit
|
|
18
|
-
# key wins; with no order_id we fall back to the per-enqueue job_id (returning
|
|
19
|
-
# nil lets with_idempotency apply that default).
|
|
20
|
-
def idempotency_key_for(options)
|
|
21
|
-
return options[:idempotency_key] if options[:idempotency_key].present?
|
|
22
|
-
return nil if options[:order_id].blank?
|
|
23
|
-
|
|
24
|
-
"order:#{options[:order_id]}:hold:#{adjustments_fingerprint(options[:inventory_id_and_quantities])}"
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Readable, order-independent fingerprint of the adjustment payload, e.g. "1=2,2=-1:locked".
|
|
28
|
-
# Delimiter hierarchy: ":" separates key fields, "," separates adjustments, "=" pairs id/qty.
|
|
29
|
-
#
|
|
30
|
-
# The pool is part of the identity: a mixed order can carry a public and a locked adjustment
|
|
31
|
-
# with the same inventory_id and quantity, and without this they fingerprint identically — so
|
|
32
|
-
# one of the two would be dropped as a duplicate and its counter never mirrored.
|
|
33
|
-
def adjustments_fingerprint(adjustments)
|
|
34
|
-
Array(adjustments)
|
|
35
|
-
.map { |adj| adj.with_indifferent_access.values_at(:inventory_id, :quantity, :locked) }
|
|
36
|
-
.sort_by { |inventory_id, quantity, locked| [inventory_id.to_s, quantity.to_s, locked.to_s] }
|
|
37
|
-
.map { |inventory_id, quantity, locked| locked ? "#{inventory_id}=#{quantity}:locked" : "#{inventory_id}=#{quantity}" }
|
|
38
|
-
.join(',')
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# :order_id, :inventory_id_and_quantities, :caller_source
|
|
16
|
+
# :order_id, :inventory_id_and_quantities, :caller_source, :idempotency_key
|
|
42
17
|
#
|
|
43
|
-
# :order_id is
|
|
44
|
-
# processing, though it's not used in the adjustment itself.
|
|
18
|
+
# :order_id is required for tracing, though it's not used in the adjustment itself.
|
|
45
19
|
# :caller_source is a string like "ClassName#method" identifying who enqueued the job.
|
|
46
20
|
def bulk_adjust_quantities_on_hold(options = {})
|
|
47
21
|
raise ArgumentError, 'order_id is required' if options[:order_id].blank?
|