Skip to content

Commit 7c2ba10

Browse files
authored
Add Joatu-style requests and offers system (#940)
## Summary - add BetterTogether::Joatu models for offers, requests, and agreements - implement matchmaking service to connect compatible offers and requests - cover new system with unit and feature specs ## Testing - `bin/codex_style_guard` *(fails: error sending request for selenium chrome driver)* - `bundle exec rubocop` - `bundle exec bundler-audit --update` - `bundle exec brakeman -q -w2` - `bin/ci` *(no examples found)* - `bundle exec rspec` *(fails: error sending request for selenium chrome driver)* ------ https://chatgpt.com/codex/tasks/task_e_6892367c6b508321938d3a3bbc1e4319
2 parents 6183799 + 5c596b2 commit 7c2ba10

File tree

148 files changed

+5219
-307
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

148 files changed

+5219
-307
lines changed

README.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,26 @@ This project embodies our vision of a world where collaboration leads to greater
1010

1111
This project is the core community building portion of the Better Together platform.
1212

13+
## Joatu Requests and Offers
14+
15+
The Joatu module enables community members to post service **requests** and **offers** and to create agreements between them.
16+
17+
### Endpoints
18+
19+
- `POST /:locale/joatu/requests` — create a request
20+
- `POST /:locale/joatu/offers` — create an offer
21+
- `POST /:locale/joatu/agreements` — create an agreement between an offer and a request
22+
- `POST /:locale/joatu/agreements/:id/reject` — reject an agreement
23+
24+
Each endpoint expects parameters nested under the matching resource name. For example:
25+
26+
```bash
27+
curl -X POST /en/joatu/requests \\
28+
-d 'request[name]=Repair help' \\
29+
-d 'request[description]=Need bike fixes' \\
30+
-d 'request[creator_id]=<person_uuid>'
31+
```
32+
1333
## Dependencies
1434

1535
In addition to other dependencies, the Better Together Community Engine relies on Action Text and Action Storage, which are part of the Rails framework. These dependencies are essential for handling rich text content and file storage within the platform.

app/assets/stylesheets/better_together/forms.scss

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
font-weight: bold;
66
}
77

8+
.form-label {
9+
font-weight: 500;
10+
}
11+
812
.bt-mb-3 {
913
margin-bottom: 1rem;
1014
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# frozen_string_literal: true
2+
3+
# app/builders/better_together/category_builder.rb
4+
module BetterTogether
5+
# Builder to create initial event and Joatu categories
6+
class CategoryBuilder < Builder
7+
class << self
8+
# Seed default categories for events and Joatu offers/requests
9+
def seed_data
10+
I18n.with_locale(:en) do
11+
build_event_categories
12+
build_joatu_categories
13+
end
14+
end
15+
16+
# Define event categories
17+
def build_event_categories
18+
::BetterTogether::EventCategory.create!(
19+
[
20+
{ name_en: 'Conference', position: 0 },
21+
{ name_en: 'Meetup', position: 1 },
22+
{ name_en: 'Workshop', position: 2 },
23+
{ name_en: 'Webinar', position: 3 }
24+
]
25+
)
26+
end
27+
28+
# Define Joatu offer/request categories
29+
def build_joatu_categories # rubocop:todo Metrics/MethodLength
30+
::BetterTogether::Joatu::Category.create!(
31+
[
32+
{ name_en: 'Accommodation' },
33+
{ name_en: 'Childcare' },
34+
{ name_en: 'Cleanup & Repairs' },
35+
{ name_en: 'Emergency Supplies' },
36+
{ name_en: 'Evacuation Housing' },
37+
{ name_en: 'Food & Water' },
38+
{ name_en: 'Medical Assistance' },
39+
{ name_en: 'Other' },
40+
{ name_en: 'Pet Care' },
41+
{ name_en: 'Platform Invitations' }, # Added for internal/platform-related offers
42+
{ name_en: 'Translation' },
43+
{ name_en: 'Transportation' },
44+
{ name_en: 'Volunteers' }
45+
]
46+
.sort_by { |attrs| attrs[:name_en] }
47+
.each_with_index
48+
.map { |attrs, idx| attrs.merge(position: idx) }
49+
)
50+
end
51+
52+
# Remove existing categories
53+
def clear_existing
54+
::BetterTogether::Category.delete_all
55+
end
56+
end
57+
end
58+
end
Lines changed: 284 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,284 @@
1+
# frozen_string_literal: true
2+
3+
# app/builders/better_together/joatu_demo_builder.rb
4+
5+
module BetterTogether
6+
# Seeds a realistic demo dataset for the Joatu exchange system
7+
class JoatuDemoBuilder < Builder # rubocop:todo Metrics/ClassLength
8+
DEMO_TAG = '[Demo]'
9+
10+
class << self
11+
def seed_data
12+
I18n.with_locale(:en) do
13+
ensure_categories!
14+
15+
people = build_people
16+
community = build_demo_community(creator: people.first)
17+
18+
addresses = build_addresses
19+
20+
requests = build_requests(people:, community:, addresses:)
21+
offers = build_offers(people:, community:, addresses:)
22+
23+
build_agreements(requests:, offers:)
24+
end
25+
end
26+
27+
# Cautious clean-up: only removes demo-tagged data created by this builder
28+
# rubocop:todo Metrics/MethodLength
29+
def clear_existing # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
30+
# Agreements first due to FK
31+
::BetterTogether::Joatu::Agreement
32+
.joins(:offer)
33+
.where("#{::BetterTogether::Joatu::Offer.table_name}.id IN (?)",
34+
demo_offers.select(:id))
35+
.delete_all
36+
37+
::BetterTogether::Joatu::Agreement
38+
.joins(:request)
39+
.where("#{::BetterTogether::Joatu::Request.table_name}.id IN (?)",
40+
demo_requests.select(:id))
41+
.delete_all
42+
43+
demo_offers.delete_all
44+
demo_requests.delete_all
45+
46+
demo_community&.destroy
47+
demo_people.delete_all
48+
end
49+
# rubocop:enable Metrics/MethodLength
50+
51+
private
52+
53+
# -- Core builders --
54+
55+
def ensure_categories!
56+
return if ::BetterTogether::Joatu::Category.exists?
57+
58+
::BetterTogether::CategoryBuilder.seed_data
59+
end
60+
61+
def build_people
62+
names = [
63+
'Ava Patel', 'Liam Nguyen', 'Maya Chen', 'Noah Garcia',
64+
'Sophia Ahmed', 'Ethan Rossi', 'Zoe Kim', 'Oliver Dubois'
65+
]
66+
67+
names.map do |name|
68+
::BetterTogether::Person.find_or_create_by!(identifier: identifier_for(name)) do |p|
69+
p.name = name
70+
end
71+
end
72+
end
73+
74+
def build_demo_community(creator:)
75+
::BetterTogether::Community.find_or_create_by!(identifier: 'joatu-demo') do |c|
76+
c.name_en = 'Joatu Demo Community'
77+
c.description_en = 'A sample community to scope Joatu exchanges.'
78+
c.creator = creator
79+
c.privacy = 'public'
80+
end
81+
end
82+
83+
def build_addresses
84+
[
85+
{ line1: '123 Harbour Rd', city_name: 'St. John\'s', state_province_name: 'NL', postal_code: 'A1A 1A1' },
86+
{ line1: '42 Elm Street', city_name: 'Halifax', state_province_name: 'NS', postal_code: 'B3H 2Y9' },
87+
{ line1: '77 Maple Avenue', city_name: 'Montreal', state_province_name: 'QC', postal_code: 'H2X 3V9' },
88+
{ line1: '900 King Street', city_name: 'Toronto', state_province_name: 'ON', postal_code: 'M5V 1G4' },
89+
{ line1: '12 Oak Crescent', city_name: 'Calgary', state_province_name: 'AB', postal_code: 'T2P 3N9' }
90+
].map do |attrs|
91+
::BetterTogether::Address.create!(attrs.merge(physical: true, postal: false, country_name: 'Canada'))
92+
end
93+
end
94+
95+
# rubocop:todo Metrics/AbcSize
96+
def build_requests(people:, community:, addresses:) # rubocop:todo Metrics/MethodLength
97+
cat = ->(name) { find_category(name) }
98+
99+
data = [
100+
{
101+
name: 'Home-cooked meals for seniors',
102+
desc: 'Requesting daily dinners for two seniors recovering post-surgery.',
103+
categories: [cat.call('Food & Water')],
104+
urgency: 'high'
105+
},
106+
{
107+
name: 'School pickup for two kids',
108+
desc: 'Help needed for weekday school pickup for 2 children.',
109+
categories: [cat.call('Childcare'), cat.call('Transportation')],
110+
urgency: 'normal'
111+
},
112+
{
113+
name: 'Minor roof repair post-storm',
114+
desc: 'Shingles lifted in last storm; need minor roof patch.',
115+
categories: [cat.call('Cleanup & Repairs')],
116+
urgency: 'critical'
117+
},
118+
{
119+
name: 'Spanish translation for clinic visit',
120+
desc: 'Looking for Spanish interpretation for a medical appointment.',
121+
categories: [cat.call('Translation'), cat.call('Medical Assistance')],
122+
urgency: 'high'
123+
},
124+
{
125+
name: 'Temporary housing for 3 nights',
126+
desc: 'Family of 3 needs a place to stay after flooding.',
127+
categories: [cat.call('Evacuation Housing'), cat.call('Accommodation')],
128+
urgency: 'high'
129+
}
130+
]
131+
132+
data.map.with_index do |row, i|
133+
::BetterTogether::Joatu::Request.create!(
134+
name_en: "#{DEMO_TAG} #{row[:name]}",
135+
description_en: row[:desc],
136+
creator: people.sample,
137+
categories: row[:categories].compact,
138+
status: 'open',
139+
urgency: row[:urgency],
140+
address: addresses[i % addresses.size],
141+
target: community
142+
)
143+
end
144+
end
145+
# rubocop:enable Metrics/AbcSize
146+
147+
# rubocop:todo Metrics/AbcSize
148+
def build_offers(people:, community:, addresses:) # rubocop:todo Metrics/MethodLength
149+
cat = ->(name) { find_category(name) }
150+
151+
data = [
152+
{
153+
name: 'Batch-cooked meals available',
154+
desc: 'Cooking extra dinners this week; can deliver locally.',
155+
categories: [cat.call('Food & Water')],
156+
urgency: 'normal'
157+
},
158+
{
159+
name: 'Evening rides with minivan',
160+
desc: 'Offering weekday evening rides; car seats available.',
161+
categories: [cat.call('Transportation')],
162+
urgency: 'low'
163+
},
164+
{
165+
name: 'Handyman for minor repairs',
166+
desc: 'Experienced with basic home repairs; evenings/weekends.',
167+
categories: [cat.call('Cleanup & Repairs')],
168+
urgency: 'normal'
169+
},
170+
{
171+
name: 'Bilingual Spanish interpreter',
172+
desc: 'Fluent in Spanish and English; can accompany to appointments.',
173+
categories: [cat.call('Translation')],
174+
urgency: 'normal'
175+
},
176+
{
177+
name: 'Guest room available',
178+
desc: 'Quiet room with private bath for short-term stays.',
179+
categories: [cat.call('Accommodation'), cat.call('Evacuation Housing')],
180+
urgency: 'high'
181+
}
182+
]
183+
184+
data.map.with_index do |row, i|
185+
::BetterTogether::Joatu::Offer.create!(
186+
name_en: "#{DEMO_TAG} #{row[:name]}",
187+
description_en: row[:desc],
188+
creator: people.sample,
189+
categories: row[:categories].compact,
190+
status: 'open',
191+
urgency: row[:urgency],
192+
address: addresses[(i + 2) % addresses.size],
193+
target: community
194+
)
195+
end
196+
end
197+
# rubocop:enable Metrics/AbcSize
198+
199+
# rubocop:todo Metrics/MethodLength
200+
def build_agreements(requests:, offers:) # rubocop:todo Metrics/AbcSize, Metrics/MethodLength
201+
# Try to pair similar categories for realism
202+
pair = lambda do |req_name_contains:, off_name_contains:, status: :pending, terms: nil, value: nil|
203+
req = requests.find { |r| r.name.include?(req_name_contains) }
204+
off = offers.find { |o| o.name.include?(off_name_contains) }
205+
return unless req && off
206+
207+
agr = ::BetterTogether::Joatu::Agreement.create!(
208+
offer: off,
209+
request: req,
210+
terms: terms,
211+
value: value,
212+
status: 'pending'
213+
)
214+
215+
case status
216+
when :accepted
217+
agr.accept!
218+
when :rejected
219+
agr.reject!
220+
end
221+
end
222+
223+
pair.call(
224+
req_name_contains: 'meals',
225+
off_name_contains: 'meals',
226+
status: :accepted,
227+
terms: 'Deliver dinners Mon–Fri for 1 week',
228+
value: '6 credits'
229+
)
230+
231+
pair.call(
232+
req_name_contains: 'School pickup',
233+
off_name_contains: 'rides',
234+
status: :pending,
235+
terms: 'Pickup at 3pm, Mon–Thu',
236+
value: 'fuel cost only'
237+
)
238+
239+
pair.call(
240+
req_name_contains: 'roof repair',
241+
off_name_contains: 'Handyman',
242+
status: :accepted,
243+
terms: 'Patch shingles and inspect attic',
244+
value: 'no cost'
245+
)
246+
247+
pair.call(
248+
req_name_contains: 'Temporary housing',
249+
off_name_contains: 'Guest room',
250+
status: :rejected,
251+
terms: '3 nights, no pets',
252+
value: 'n/a'
253+
)
254+
end
255+
# rubocop:enable Metrics/MethodLength
256+
257+
# -- Helpers --
258+
259+
def find_category(name)
260+
::BetterTogether::Joatu::Category.i18n.find_or_create_by(name: name)
261+
end
262+
263+
def identifier_for(name)
264+
"#{DEMO_TAG} #{name}".parameterize
265+
end
266+
267+
def demo_people
268+
::BetterTogether::Person.where('identifier LIKE ?', "#{DEMO_TAG.downcase.tr('[]', '')}%")
269+
end
270+
271+
def demo_community
272+
::BetterTogether::Community.find_by(identifier: 'joatu-demo')
273+
end
274+
275+
def demo_offers
276+
::BetterTogether::Joatu::Offer.i18n.where('mobility_string_translations.value LIKE ?', "%#{DEMO_TAG}%")
277+
end
278+
279+
def demo_requests
280+
::BetterTogether::Joatu::Request.i18n.where('mobility_string_translations.value LIKE ?', "%#{DEMO_TAG}%")
281+
end
282+
end
283+
end
284+
end

0 commit comments

Comments
 (0)