Skip to content

Commit ff81597

Browse files
authored
✨🥗 Marketplace: VendorRepresentative manages Products (#2181)
- #2044 OK this should probably be split into a number of smaller steps, each with reasonable test coverage; but I wanted to get something working so we can turn the keys over to the folks at Oaklandia. This makes it so: - A `Neighborhood` `Operator` or `Space` `Member` can add a `VendorRepresentative` to a `Marketplace` - The `VendorRepresentative` can add, remove, and update `Products` YOLO YOLO YOLO * 🥗 `Marketplace`: Test adding `VendorRepresentative` - #2044 Testings good, actually
1 parent c08c6ba commit ff81597

17 files changed

+248
-2
lines changed

app/components/svg_component.rb

+10
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ class SvgComponent < ApplicationComponent
88
#
99
# Format: { symbol: method returning path for symbol }
1010
ICON_MAPPINGS = {
11+
cake: :cake,
1112
cart: :cart,
1213
money: :money,
1314
exclamation_triangle: :exclamation_triangle,
@@ -119,4 +120,13 @@ def pencil
119120
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
120121
SVG
121122
end
123+
124+
def cake
125+
<<~SVG
126+
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
127+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 8.25v-1.5m0 1.5c-1.355 0-2.697.056-4.024.166C6.845 8.51 6 9.473 6 10.608v2.513m6-4.871c1.355 0 2.697.056 4.024.166C17.155 8.51 18 9.473 18 10.608v2.513M15 8.25v-1.5m-6 1.5v-1.5m12 9.75-1.5.75a3.354 3.354 0 0 1-3 0 3.354 3.354 0 0 0-3 0 3.354 3.354 0 0 1-3 0 3.354 3.354 0 0 0-3 0 3.354 3.354 0 0 1-3 0L3 16.5m15-3.379a48.474 48.474 0 0 0-6-.371c-2.032 0-4.034.126-6 .371m12 0c.39.049.777.102 1.163.16 1.07.16 1.837 1.094 1.837 2.175v5.169c0 .621-.504 1.125-1.125 1.125H4.125A1.125 1.125 0 0 1 3 20.625v-5.17c0-1.08.768-2.014 1.837-2.174A47.78 47.78 0 0 1 6 13.12M12.265 3.11a.375.375 0 1 1-.53 0L12 2.845l.265.265Zm-3 0a.375.375 0 1 1-.53 0L9 2.845l.265.265Zm6 0a.375.375 0 1 1-.53 0L15 2.845l.265.265Z" />
128+
</svg>
129+
130+
SVG
131+
end
122132
end

app/furniture/marketplace/breadcrumbs.rb

+10
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,16 @@
7474
link t("marketplace.products.edit.link_to", name: product.name), product.location(:edit)
7575
end
7676

77+
crumb :marketplace_vendor_representatives do |marketplace|
78+
parent :edit_marketplace, marketplace
79+
link t("marketplace.vendor_representatives.index.link_to"), marketplace.location(child: :vendor_representatives)
80+
end
81+
82+
crumb :new_marketplace_vendor_representative do |vendor_representative|
83+
parent :edit_marketplace, vendor_representative.marketplace
84+
link t("marketplace.vendor_representatives.new.link_to"), marketplace.location(:new, child: :vendor_representative)
85+
end
86+
7787
crumb :marketplace_delivery_areas do |marketplace|
7888
parent :edit_marketplace, marketplace
7989
link t("marketplace.delivery_areas.index.link_to"), marketplace.location(child: :delivery_areas)

app/furniture/marketplace/locales/en.yml

+9
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,15 @@ en:
114114
link_to: Edit Tax Rate '%{name}'
115115
destroy:
116116
link_to: Remove Tax Rate '%{name}'
117+
vendor_representatives:
118+
index:
119+
link_to: "Vendor Representatives"
120+
new:
121+
link_to: "Add a Representative"
122+
create:
123+
success: "Added Representative '%{email_address}'"
124+
update:
125+
success: "Updated Representative '%{email_address}'"
117126
cart_products:
118127
destroy:
119128
success: "Removed %{quantity} %{product} from Cart"

app/furniture/marketplace/management_component.html.erb

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
<%= render button({child: :orders}, icon: :cart) if policy(marketplace.orders).index? %>
2121
<%= render button({child: :notification_methods}, icon: :bell) if policy(marketplace.notification_methods).index? %>
2222
<%= render button({child: :flyer}, icon: :receipt_percent) if policy(marketplace.flyer).show? %>
23+
<%= render button({child: :vendor_representatives}, icon: :cake) if policy(marketplace.vendor_representatives).index? %>
2324
</nav>
2425
<% end %>
2526
<% end %>

app/furniture/marketplace/marketplace.rb

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class Marketplace < Furniture
1616
has_many :delivery_areas, inverse_of: :marketplace, dependent: :destroy
1717

1818
has_many :notification_methods, inverse_of: :marketplace, dependent: :destroy
19+
has_many :vendor_representatives, inverse_of: :marketplace, dependent: :destroy
1920

2021
setting :stripe_account
2122
alias_method :vendor_stripe_account, :stripe_account

app/furniture/marketplace/marketplace_policy.rb

+3-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ def show?
88
end
99

1010
def create?
11-
current_person.operator? || current_person.member_of?(marketplace.space)
11+
current_person.operator? ||
12+
current_person.member_of?(marketplace.space) ||
13+
marketplace.vendor_representatives.exists?(person: current_person)
1214
end
1315

1416
alias_method :update?, :create?

app/furniture/marketplace/product_policy.rb

+2
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ def permitted_attributes(_params = nil)
99

1010
def update?
1111
return false unless current_person.authenticated?
12+
return true if marketplace.vendor_representatives.exists?(person: current_person)
1213

1314
super
1415
end
16+
1517
alias_method :create?, :update?
1618

1719
def show?

app/furniture/marketplace/routes.rb

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ def self.append_routes(router)
1717
router.resource :stripe_account, only: [:show, :new, :create]
1818
router.resources :stripe_events
1919
router.resources :tax_rates
20+
router.resources :vendor_representatives
2021
router.resources :payment_settings, only: [:index]
2122
end
2223
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
class Marketplace
2+
class VendorRepresentative < Record
3+
self.table_name = :marketplace_vendor_representatives
4+
5+
belongs_to :marketplace, inverse_of: :vendor_representatives
6+
has_one :room, through: :marketplace
7+
belongs_to :person, optional: true
8+
9+
attribute :email_address, :string
10+
validates :email_address, uniqueness: true, presence: true
11+
12+
location(parent: :marketplace)
13+
14+
def claimed?
15+
person.present?
16+
end
17+
18+
def claimable?
19+
!claimed? && matching_person.present?
20+
end
21+
22+
def matching_person
23+
Person.joins(:authentication_methods).find_by(authentication_methods: {contact_location: email_address})
24+
end
25+
end
26+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# frozen_string_literal: true
2+
3+
class Marketplace
4+
class VendorRepresentativePolicy < Policy
5+
alias_method :vendor_representative, :object
6+
def permitted_attributes(_params = nil)
7+
%i[email_address person_id]
8+
end
9+
10+
def update?
11+
return false unless current_person.authenticated?
12+
13+
super
14+
end
15+
alias_method :create?, :update?
16+
17+
def show?
18+
true
19+
end
20+
21+
class Scope < ApplicationScope
22+
def resolve
23+
scope.all
24+
end
25+
end
26+
end
27+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<%= render CardComponent.new(dom_id: dom_id(vendor_representative)) do %>
2+
<%= form_with model: vendor_representative.location do |f| %>
3+
<%= render "email_field", { attribute: :email_address, form: f } %>
4+
5+
<%= f.submit %>
6+
<%- end %>
7+
<%- end %>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<%- breadcrumb :marketplace_vendor_representatives, marketplace %>
2+
3+
<%= render CardComponent.new do |card| %>
4+
5+
<%- marketplace.vendor_representatives.each do |vendor_representative| %>
6+
<div id="<%= dom_id(vendor_representative)%>" class="flex flex-row gap-3">
7+
<span class="flex-initial">
8+
<%- if vendor_representative.claimed? %>
9+
10+
<%- else %>
11+
<%- if vendor_representative.claimable? %>
12+
<%= button_to "👍", vendor_representative.location, method: :put, params: { vendor_representative: { person_id: vendor_representative.matching_person.id } }%>
13+
<%- else %>
14+
<span class="button">🛌</span>
15+
<%- end %>
16+
<%- end %>
17+
</span>
18+
<span class="flex-grow">
19+
<%= vendor_representative.email_address %>
20+
</span>
21+
<span class="flex-initial">
22+
<%- if policy(vendor_representative).destroy? %>
23+
<%= button_to "🗑️", vendor_representative.location, method: :delete %>
24+
<%- end %>
25+
</span>
26+
</div>
27+
<%- end %>
28+
29+
<%- card.with_footer(variant: :action_bar) do %>
30+
<%- new_vendor_representative = marketplace.vendor_representatives.new %>
31+
<%- if policy(new_vendor_representative).create? %>
32+
<%= link_to t("marketplace.vendor_representatives.new.link_to"), marketplace.location(:new, child: :vendor_representative), class: "button w-full" %>
33+
<%- end %>
34+
<%- end %>
35+
<%- end %>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<%- breadcrumb :new_marketplace_vendor_representative, vendor_representative %>
2+
3+
<%= render "form", vendor_representative: vendor_representative %>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
class Marketplace
4+
class VendorRepresentativesController < Controller
5+
expose :vendor_representative, scope: -> { vendor_representatives }, model: VendorRepresentative
6+
expose :vendor_representatives, -> { policy_scope(marketplace.vendor_representatives) }
7+
8+
def new
9+
authorize(vendor_representative)
10+
end
11+
12+
def create
13+
authorize(vendor_representative).save
14+
15+
if vendor_representative.persisted?
16+
redirect_to marketplace.location(child: :vendor_representatives), notice: t(".success", email_address: vendor_representative.email_address)
17+
else
18+
render :new, status: :unprocessable_entity
19+
end
20+
end
21+
22+
def index
23+
skip_authorization
24+
end
25+
26+
def update
27+
if authorize(vendor_representative).update(vendor_representative_params)
28+
redirect_to marketplace.location(child: :vendor_representatives), notice: t(".success", email_address: vendor_representative.email_address)
29+
30+
else
31+
redirect_to marketplace.location(child: :vendor_representatives), notice: t(".failure", email_address: vendor_representative.email_address)
32+
33+
end
34+
end
35+
36+
def destroy
37+
authorize(vendor_representative).destroy
38+
redirect_to marketplace.location(child: :vendor_representatives), notice: t(".success", name: vendor_representative.email_address)
39+
end
40+
41+
def vendor_representative_params
42+
policy(VendorRepresentative).permit(params.require(:vendor_representative))
43+
end
44+
end
45+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
class MarketplaceCreateVendorRepresentatives < ActiveRecord::Migration[7.1]
2+
def change
3+
create_table :marketplace_vendor_representatives, id: :uuid do |t|
4+
t.references :marketplace, type: :uuid, foreign_key: {to_table: :furnitures}
5+
t.references :person, type: :uuid, foreign_key: true, null: true
6+
7+
t.string :email_address
8+
t.timestamps
9+
end
10+
end
11+
end

db/schema.rb

+13-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
# It's strongly recommended that you check this file into your version control system.
1212

13-
ActiveRecord::Schema[7.1].define(version: 2024_02_01_014607) do
13+
ActiveRecord::Schema[7.1].define(version: 2024_02_07_040004) do
1414
# These are extensions that must be enabled in order to support this database
1515
enable_extension "pgcrypto"
1616
enable_extension "plpgsql"
@@ -233,6 +233,16 @@
233233
t.index ["marketplace_id"], name: "index_marketplace_tax_rates_on_marketplace_id"
234234
end
235235

236+
create_table "marketplace_vendor_representatives", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
237+
t.uuid "marketplace_id"
238+
t.uuid "person_id"
239+
t.string "email_address"
240+
t.datetime "created_at", null: false
241+
t.datetime "updated_at", null: false
242+
t.index ["marketplace_id"], name: "index_marketplace_vendor_representatives_on_marketplace_id"
243+
t.index ["person_id"], name: "index_marketplace_vendor_representatives_on_person_id"
244+
end
245+
236246
create_table "media", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
237247
t.datetime "created_at", null: false
238248
t.datetime "updated_at", null: false
@@ -324,6 +334,8 @@
324334
add_foreign_key "marketplace_shoppers", "people"
325335
add_foreign_key "marketplace_tax_rates", "furnitures", column: "marketplace_id"
326336
add_foreign_key "marketplace_tax_rates", "spaces", column: "bazaar_id"
337+
add_foreign_key "marketplace_vendor_representatives", "furnitures", column: "marketplace_id"
338+
add_foreign_key "marketplace_vendor_representatives", "people"
327339
add_foreign_key "memberships", "invitations"
328340
add_foreign_key "rooms", "media", column: "hero_image_id"
329341
add_foreign_key "space_agreements", "spaces"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
require "rails_helper"
2+
3+
# @see https://github.com/zinc-collective/convene/issues/2044
4+
describe "Marketplace: Vendor Representatives", type: :system do
5+
let(:space) { create(:space, :with_entrance, :with_members) }
6+
let(:marketplace) { create(:marketplace, :ready_for_shopping, room: space.entrance) }
7+
8+
before do
9+
sign_in(space.members.first, space)
10+
end
11+
12+
describe "Adding a Vendor Representative" do
13+
it "Requires a Member confirm the Vendor" do # rubocop:disable RSpec/ExampleLength
14+
visit(polymorphic_path(marketplace.location(child: :vendor_representatives)))
15+
click_link("Add a Representative")
16+
fill_in("Email address", with: "[email protected]")
17+
expect do
18+
click_button("Create")
19+
expect(page).to have_content("Added Representative '[email protected]'")
20+
marketplace.reload
21+
end.to change(marketplace.vendor_representatives, :count).by(1)
22+
representative_milton = marketplace.vendor_representatives.find_by(email_address: "[email protected]")
23+
24+
within("##{dom_id(representative_milton)}") do
25+
expect(page).to have_content("🛌")
26+
end
27+
expect(representative_milton).not_to be_claimable
28+
expect(representative_milton).not_to be_claimed
29+
milton = create(:authentication_method, contact_location: "[email protected]").person
30+
31+
expect(representative_milton).to be_claimable
32+
visit(polymorphic_path(marketplace.location(child: :vendor_representatives)))
33+
34+
expect do
35+
within("##{dom_id(representative_milton)}") do
36+
click_button("👍")
37+
representative_milton.reload
38+
end
39+
end.to change(representative_milton, :claimed?).to(true)
40+
41+
expect(representative_milton.person).to eq(milton)
42+
end
43+
end
44+
end

0 commit comments

Comments
 (0)