spree_cm_commissioner 2.14.7 → 2.14.8
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/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/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/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/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 +1 -0
- metadata +8 -8
- 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: ceb90915f56d7b2621b44c00abe7b65adbdf5093f566d3b801333496c3333402
|
|
4
|
+
data.tar.gz: 418a9332f9ddf34711f4c251c5c6040388e191efa8f2f8eefc11d12777a6f79a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6ef4fcfc3e0feb5af8d54f06a7321e1369a35b87f625d8465a70edb5c2ac2502e180812ba60fb3d5d081bc746367bcb756e9124416719f73af0143cea86242b6
|
|
7
|
+
data.tar.gz: 46af2a2e96874f557e5921b671838148b148791a408c0c55b26d7647fb4fc1fc11ea775d25ee5998da402d0f7cac4b00374bb269ee21eaef5edc088bb1053bdd
|
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
|
|
@@ -87,7 +87,7 @@ module SpreeCmCommissioner
|
|
|
87
87
|
after_transition from: :requested, to: :accepted, do: :decrement_user_request_orders_count
|
|
88
88
|
after_transition to: :accepted, do: :send_order_accepted_app_notification_to_user
|
|
89
89
|
after_transition to: :accepted, do: :send_order_accepted_telegram_alert_to_store
|
|
90
|
-
after_transition to: :accepted,
|
|
90
|
+
after_transition to: :accepted, unless: :confirmation_delivered?, do: :deliver_order_confirmation_email
|
|
91
91
|
|
|
92
92
|
event :reject do
|
|
93
93
|
transition from: :requested, to: :rejected
|
|
@@ -144,14 +144,6 @@ module SpreeCmCommissioner
|
|
|
144
144
|
subscription? || prepaid_top_up_order?
|
|
145
145
|
end
|
|
146
146
|
|
|
147
|
-
# override
|
|
148
|
-
def deliver_order_confirmation_email
|
|
149
|
-
return if requested?
|
|
150
|
-
return if any_app_only_products?
|
|
151
|
-
|
|
152
|
-
super
|
|
153
|
-
end
|
|
154
|
-
|
|
155
147
|
def increment_user_request_orders_count
|
|
156
148
|
return if user.nil?
|
|
157
149
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
module SalesChannelBitwise
|
|
3
|
+
extend ActiveSupport::Concern
|
|
4
|
+
|
|
5
|
+
# Append only: stored values are bit masks. Names follow the `channel_direct` /
|
|
6
|
+
# `channel_platform` channel types in docs/concepts/multi-channel-pricing.md.
|
|
7
|
+
BIT_SALES_CHANNEL = {
|
|
8
|
+
direct: 0b01, # the tenant's own app/web
|
|
9
|
+
platform: 0b10 # the BookMe+ marketplace
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
included do
|
|
13
|
+
validates :sales_channel, numericality: { greater_than: 0 }
|
|
14
|
+
|
|
15
|
+
# Runs after ProductDecorator#set_tenant: a product without a tenant has no direct channel to sell on.
|
|
16
|
+
before_save -> { self.sales_channel = BIT_SALES_CHANNEL[:platform] if tenant_id.nil? }
|
|
17
|
+
|
|
18
|
+
scope :sellable_on, lambda { |channel|
|
|
19
|
+
where("#{table_name}.sales_channel & ? != 0", BIT_SALES_CHANNEL.fetch(channel))
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class_methods do
|
|
24
|
+
# The channel a customer order is placed on: tenant storefront APIs set a tenant, the platform ones don't.
|
|
25
|
+
def sales_channel_for(tenant)
|
|
26
|
+
tenant.present? ? :direct : :platform
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
BIT_SALES_CHANNEL.each do |channel, bit_value|
|
|
31
|
+
define_method "sellable_on_#{channel}?" do
|
|
32
|
+
sales_channel.to_i & bit_value != 0
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Checkbox reader/writer for the dashboard: a tenant product opts in/out of being sold on BookMe+.
|
|
37
|
+
alias sellable_on_platform sellable_on_platform?
|
|
38
|
+
|
|
39
|
+
def sellable_on_platform=(value)
|
|
40
|
+
bit = BIT_SALES_CHANNEL[:platform]
|
|
41
|
+
self.sales_channel = ActiveModel::Type::Boolean.new.cast(value) ? sales_channel.to_i | bit : sales_channel.to_i & ~bit
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module SpreeCmCommissioner
|
|
2
|
+
# For images a customer uploads themselves (profile photo, ID card). Spree's own validation accepts
|
|
3
|
+
# any `image/*`, which lets formats like SVG reach libvips when variants are generated
|
|
4
|
+
# (CVE-2026-66066), so these are limited to the formats phone cameras and galleries produce.
|
|
5
|
+
# MAX_SIZE is also the `content_length_range` on the storefront/tenant presigned POST, so an
|
|
6
|
+
# oversized file is rejected by S3 before it is ever fetched back.
|
|
7
|
+
module UserUploadedImage
|
|
8
|
+
extend ActiveSupport::Concern
|
|
9
|
+
|
|
10
|
+
CONTENT_TYPES = %w[image/jpeg image/png image/webp image/heic image/heif].freeze
|
|
11
|
+
MAX_SIZE = 10.megabytes
|
|
12
|
+
|
|
13
|
+
included do
|
|
14
|
+
validates :attachment, content_type: CONTENT_TYPES, size: { less_than_or_equal_to: MAX_SIZE }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -190,6 +190,17 @@ module SpreeCmCommissioner
|
|
|
190
190
|
super
|
|
191
191
|
end
|
|
192
192
|
|
|
193
|
+
# Must live here, not in OrderStateMachine: that concern is included (below Spree::Order),
|
|
194
|
+
# while Spree::Emails::OrderDecorator is prepended above it and never calls super, so an
|
|
195
|
+
# override there is silently dead.
|
|
196
|
+
def deliver_order_confirmation_email
|
|
197
|
+
return if requested?
|
|
198
|
+
return if any_app_only_products?
|
|
199
|
+
return if products.none?(&:enable_confirmation_email?)
|
|
200
|
+
|
|
201
|
+
super
|
|
202
|
+
end
|
|
203
|
+
|
|
193
204
|
def any_app_only_products?
|
|
194
205
|
if products.loaded?
|
|
195
206
|
products.any?(&:purchasable_on_app?)
|
|
@@ -143,6 +143,7 @@ module SpreeCmCommissioner
|
|
|
143
143
|
base.store_public_metadata :enable_inventory_hold, :boolean, default: false
|
|
144
144
|
base.store_public_metadata :enable_telegram_alert, :boolean, default: true
|
|
145
145
|
base.store_public_metadata :enable_send_receipt, :boolean, default: true
|
|
146
|
+
base.store_public_metadata :enable_confirmation_email, :boolean, default: true
|
|
146
147
|
base.store_public_metadata :enable_social_sharing_email, :boolean, default: false
|
|
147
148
|
|
|
148
149
|
base.store_private_metadata :advertise_weight, :integer, default: 1
|
|
@@ -169,6 +170,9 @@ module SpreeCmCommissioner
|
|
|
169
170
|
base.belongs_to :tenant, class_name: 'SpreeCmCommissioner::Tenant'
|
|
170
171
|
base.before_save :set_tenant
|
|
171
172
|
|
|
173
|
+
# Included after set_tenant so its before_save sees the resolved tenant_id.
|
|
174
|
+
base.include SpreeCmCommissioner::SalesChannelBitwise
|
|
175
|
+
|
|
172
176
|
# Create maintaining task to purge product related caches
|
|
173
177
|
base.after_save { SpreeCmCommissioner::MaintenanceTasks::CacheInvalidation.pending.create_or_find_by(maintainable: self) }
|
|
174
178
|
|