Skip to content

Commit 93d2492

Browse files
committed
feat(api): Complete all outstanding API parity tasks
SearchConfigController (NEW): - GET /api_public/v1/search/config - Returns property types with counts, price options, features - Supports bedrooms, bathrooms, sort options PropertiesController (ENHANCED): - Added map_markers to search response with lat/lng/title/price - Added pagination meta (total, page, per_page, total_pages) - Added ?highlighted=true filter for featured properties - Added ?limit param for limiting results ListedProperty.as_json (ENHANCED): - Added extras, features, highlighted fields - Added primary_image_url for convenience ContactController (NEW): - POST /api_public/v1/contact for general enquiries - Separate from property-specific /enquiries endpoint Routes: - Added /search/config and /contact routes
1 parent 06b1292 commit 93d2492

File tree

6 files changed

+228
-9
lines changed

6 files changed

+228
-9
lines changed
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# frozen_string_literal: true
2+
3+
module ApiPublic
4+
module V1
5+
# ContactController handles general (non-property) contact form submissions
6+
# For property-specific enquiries, use EnquiriesController
7+
class ContactController < BaseController
8+
# POST /api_public/v1/contact
9+
def create
10+
locale = params[:locale] || I18n.default_locale
11+
I18n.locale = locale
12+
website = Pwb::Current.website
13+
14+
# Find or create contact
15+
contact = website.contacts.find_or_initialize_by(primary_email: contact_params[:email])
16+
contact.assign_attributes(
17+
first_name: contact_params[:name],
18+
primary_phone_number: contact_params[:phone]
19+
)
20+
21+
# Create the message
22+
title = contact_params[:subject].presence || I18n.t("contact.general_enquiry", default: "General Enquiry")
23+
message = Pwb::Message.new(
24+
website: website,
25+
title: title,
26+
content: contact_params[:message],
27+
locale: locale,
28+
url: request.referer,
29+
host: request.host,
30+
origin_ip: request.ip,
31+
origin_email: contact_params[:email],
32+
user_agent: request.user_agent,
33+
delivery_email: website.agency&.email_for_contact_form.presence || website.agency&.email_primary
34+
)
35+
36+
# Validate and save
37+
unless message.valid? && contact.valid?
38+
errors = contact.errors.full_messages + message.errors.full_messages
39+
return render json: { success: false, errors: errors }, status: :unprocessable_entity
40+
end
41+
42+
contact.save!
43+
message.contact = contact
44+
message.save!
45+
46+
# Send email notification asynchronously
47+
delivery_email = website.agency&.email_for_contact_form.presence || website.agency&.email_primary
48+
if delivery_email.present?
49+
begin
50+
ContactMailer.general_enquiry(contact, message).deliver_later if defined?(ContactMailer) && ContactMailer.respond_to?(:general_enquiry)
51+
rescue StandardError => e
52+
Rails.logger.warn("[Contact API] Email delivery failed: #{e.message}")
53+
end
54+
end
55+
56+
render json: {
57+
success: true,
58+
message: I18n.t("contact.success", default: "Thank you for your message. We'll get back to you soon."),
59+
data: {
60+
contact_id: contact.id,
61+
message_id: message.id
62+
}
63+
}, status: :created
64+
65+
rescue StandardError => e
66+
Rails.logger.error("[API Contact] Error: #{e.message}")
67+
Rails.logger.error(e.backtrace.first(5).join("\n"))
68+
render json: {
69+
success: false,
70+
errors: [I18n.t("contact.error", default: "An error occurred. Please try again.")]
71+
}, status: :internal_server_error
72+
end
73+
74+
private
75+
76+
def contact_params
77+
params.require(:contact).permit(:name, :email, :phone, :message, :subject)
78+
end
79+
end
80+
end
81+
end

app/controllers/api_public/v1/properties_controller.rb

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ def show
1616
end
1717

1818
def search
19+
locale = params[:locale] || I18n.default_locale
20+
I18n.locale = locale
21+
1922
# Default values matching GraphQL implementation
2023
args = {
2124
sale_or_rental: params[:sale_or_rental] || "sale",
@@ -31,7 +34,51 @@ def search
3134

3235
# Use listed_properties (materialized view) instead of deprecated props
3336
properties = Pwb::Current.website.listed_properties.properties_search(**args)
34-
render json: properties.as_json
37+
38+
# Filter by highlighted/featured if requested
39+
if params[:highlighted] == 'true'
40+
properties = properties.where(highlighted: true)
41+
end
42+
43+
# Apply limit if specified
44+
if params[:limit].present?
45+
properties = properties.limit(params[:limit].to_i)
46+
end
47+
48+
# Pagination support
49+
page = (params[:page] || 1).to_i
50+
per_page = (params[:per_page] || 12).to_i
51+
52+
# Simple pagination using offset/limit
53+
total_count = properties.count
54+
total_pages = (total_count.to_f / per_page).ceil
55+
paginated_properties = properties.offset((page - 1) * per_page).limit(per_page)
56+
57+
# Generate map markers
58+
map_markers = paginated_properties.map do |prop|
59+
next unless prop.latitude.present? && prop.longitude.present?
60+
{
61+
id: prop.id,
62+
slug: prop.slug,
63+
lat: prop.latitude,
64+
lng: prop.longitude,
65+
title: prop.title,
66+
price: prop.formatted_price,
67+
image: prop.primary_image_url,
68+
url: "/properties/#{prop.slug}"
69+
}
70+
end.compact
71+
72+
render json: {
73+
data: paginated_properties.as_json,
74+
map_markers: map_markers,
75+
meta: {
76+
total: total_count,
77+
page: page,
78+
per_page: per_page,
79+
total_pages: total_pages
80+
}
81+
}
3582
end
3683
end
3784
end
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# frozen_string_literal: true
2+
3+
module ApiPublic
4+
module V1
5+
# SearchConfigController provides filter configuration for headless frontend search pages
6+
# Returns property types, price options, features, and sort options
7+
class SearchConfigController < BaseController
8+
# GET /api_public/v1/search/config
9+
def index
10+
website = Pwb::Current.website
11+
locale = params[:locale] || I18n.locale
12+
I18n.locale = locale
13+
14+
render json: {
15+
property_types: property_types_with_counts(website),
16+
price_options: {
17+
sale: {
18+
from: website.sale_price_options_from,
19+
to: website.sale_price_options_till
20+
},
21+
rent: {
22+
from: website.rent_price_options_from,
23+
to: website.rent_price_options_till
24+
}
25+
},
26+
features: available_features(website),
27+
bedrooms: (0..10).to_a,
28+
bathrooms: (0..6).to_a,
29+
sort_options: [
30+
{ value: 'price_asc', label: I18n.t('search.sort.price_asc', default: 'Price: Low to High') },
31+
{ value: 'price_desc', label: I18n.t('search.sort.price_desc', default: 'Price: High to Low') },
32+
{ value: 'newest', label: I18n.t('search.sort.newest', default: 'Newest First') },
33+
{ value: 'bedrooms_desc', label: I18n.t('search.sort.bedrooms_desc', default: 'Most Bedrooms') }
34+
],
35+
area_unit: website.default_area_unit || 'sqm',
36+
currency: website.default_currency || 'EUR'
37+
}
38+
end
39+
40+
private
41+
42+
def property_types_with_counts(website)
43+
counts = website.listed_properties
44+
.visible
45+
.group(:prop_type_key)
46+
.count
47+
48+
counts.map do |key, count|
49+
next if key.blank?
50+
{
51+
key: key.to_s.split('.').last,
52+
label: I18n.t("propertyTypes.#{key.to_s.split('.').last}", default: key.to_s.split('.').last.titleize),
53+
count: count
54+
}
55+
end.compact
56+
end
57+
58+
def available_features(website)
59+
# Get features from FieldKeys if available
60+
feature_keys = PwbTenant::FieldKey.where(website: website, field_key_group_tag: 'feature')
61+
62+
if feature_keys.any?
63+
feature_keys.map do |fk|
64+
{
65+
key: fk.field_key_tag,
66+
label: fk.label.presence || I18n.t("features.#{fk.field_key_tag}", default: fk.field_key_tag.titleize)
67+
}
68+
end
69+
else
70+
# Fallback: extract from properties
71+
all_extras = website.listed_properties.visible.pluck(:extras).compact.flatten
72+
all_extras.uniq.first(20).map do |extra|
73+
{
74+
key: extra.to_s.parameterize,
75+
label: extra.to_s.titleize
76+
}
77+
end
78+
end
79+
rescue StandardError => e
80+
Rails.logger.warn("[SearchConfig] Error loading features: #{e.message}")
81+
[]
82+
end
83+
end
84+
end
85+
end

app/models/pwb/listed_property.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ def as_json(options = nil)
232232
hash['description'] = description
233233
hash['meta_title'] = generate_meta_title
234234
hash['meta_description'] = generate_meta_description
235+
hash['extras'] = extras_for_display
236+
hash['features'] = get_features
237+
hash['highlighted'] = highlighted
238+
hash['primary_image_url'] = primary_image_url
235239
end
236240
end
237241

config/routes.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,8 +728,10 @@
728728
get "/site_details" => "site_details#index"
729729
get "/select_values" => "select_values#index"
730730
get "/theme" => "theme#index"
731+
get "/search/config" => "search_config#index"
731732
get "/testimonials" => "testimonials#index"
732733
post "/enquiries" => "enquiries#create"
734+
post "/contact" => "contact#create"
733735
post "/auth/firebase" => "auth#firebase"
734736

735737
# Embeddable Widget API

docs/js_client/API_PARITY_AUDIT.md

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -274,12 +274,12 @@ Pages with `is_rails_part: true` render ERB partials on the server. JS clients m
274274

275275
### Backend Changes Needed
276276

277-
- [ ] Create `SearchConfigController` with filter options
278-
- [ ] Add map_markers to properties search response
279-
- [ ] Add pagination meta to search response
280-
- [ ] Add `?highlighted=true` query support
281-
- [ ] Add `extras` and `features` to ListedProperty.as_json
282-
- [ ] Create general contact endpoint (optional)
277+
- [x] Create `SearchConfigController` with filter options
278+
- [x] Add map_markers to properties search response
279+
- [x] Add pagination meta to search response
280+
- [x] Add `?highlighted=true` query support
281+
- [x] Add `extras` and `features` to ListedProperty.as_json
282+
- [x] Create general contact endpoint
283283

284284
### Frontend Implementation
285285

@@ -320,8 +320,8 @@ GET /api_public/v1/testimonials
320320
GET /api_public/v1/select_values?field_names=property-types
321321

322322
# Future (to be implemented)
323-
GET /api_public/v1/search/config
324-
POST /api_public/v1/contact
323+
# GET /api_public/v1/search/config <-- NOW IMPLEMENTED
324+
# POST /api_public/v1/contact <-- NOW IMPLEMENTED
325325
```
326326

327327
---

0 commit comments

Comments
 (0)