Skip to content

Listings Domain

Overview

The Listings domain manages individual service listings in the Wedissimo marketplace. While Vendors represent the service providers, Listings represent the specific services they offer.

Module Location: modules/Listing/Status: ✅ Fully migrated to modular architecture

Domain Responsibilities

  • Service listing creation and management
  • Pricing and package information
  • Listing status and visibility control
  • Featured listings promotion
  • Listing categories and tagging
  • Media gallery relationships (managing order and collection via polymorphic pivot)
  • Search integration
  • Listing analytics and views

Note: Media files themselves are managed by the core Media model (app/Models/Media.php). A dedicated Media domain module is planned for the future. The Listing module only manages the relationship between listings and media (order, collection) via the polymorphic mediables pivot table.

Core Entities

Listing

Primary Entity: Represents a specific service offering from a vendor

Key Attributes:

  • id (UUID)
  • vendor_id (FK to Vendor)
  • title, slug
  • description, excerpt
  • price (base price), price_extras_description, max_hours, currency
  • location, based_in
  • latitude, longitude, service_center (PostGIS geography point)
  • service_radius_miles, travel_included_miles
  • coverage_min_lat, coverage_min_lng, coverage_max_lat, coverage_max_lng (legacy)
  • status (draft, pending, published, archived)
  • verified (boolean)
  • featured (boolean)
  • view_count, rating, review_count
  • years_experience
  • featured_image_id
  • package_inclusions (JSON)
  • external_reviews (JSON)
  • SEO fields: meta_title, meta_description, focus_keyword, canonical_url
  • Open Graph fields: og_title, og_description, og_image_url
  • Twitter Card fields: twitter_title, twitter_description, twitter_image_url
  • Booking configuration: calendar_sync_url, max_booking_days, min_booking_days, booking_lead_time, booking_window_days, requires_approval
  • published_at, created_at, updated_at, deleted_at
  • wp_id (WordPress migration tracking)

Model Location: modules/Listing/Models/Listing.php

Listing Status

Value object representing listing visibility:

  • draft - Being created, not yet published
  • pending - Awaiting approval
  • published - Live and visible in marketplace
  • archived - Permanently removed from search

Domain Services

ListingService

Location: modules/Listing/Services/ListingService.php

Responsibilities:

  • Multi-strategy listing search (Typesense, PostGIS, Eloquent)
  • Geographic filtering with PostGIS spatial queries
  • Google Places API integration for region-based searches
  • Listing retrieval by ID or slug
  • Search region metadata for frontend mapping

Key Methods:

php
class ListingService
{
    // Multi-strategy search with geographic filtering
    public function search(array $filters, int $perPage = 15): LengthAwarePaginator;

    // Get search region metadata (for map visualization)
    public function getLastSearchRegion(): ?array;

    // Retrieve listings
    public function getById(string $id, bool $publishedOnly = true): ?Listing;
    public function getBySlug(string $slug, bool $publishedOnly = true): ?Listing;
    public function getBySlugOrId(string $slugOrId, bool $publishedOnly = true): ?Listing;
}

Search Strategies:

  1. Typesense Search - When query parameter is provided (full-text search)
  2. PostGIS Search - When geographic filters are provided (Google Place ID or lat/lng)
  3. Eloquent Search - Fallback for simple filtering (categories, tags, price, etc.)

CategoryService

Location: modules/Listing/Services/CategoryService.php

Responsibilities:

  • Manage listing categories
  • Category search and autocomplete
  • Category hierarchy management

TagService

Location: modules/Listing/Services/TagService.php

Responsibilities:

  • Manage listing tags
  • Tag search and autocomplete
  • Tag normalization and deduplication

API Endpoints

Public Endpoints

http
GET    /api/v1/listings/search              # Search listings with filters
GET    /api/v1/listings/{slugOrId}          # Get listing details by slug or ID
GET    /api/v1/listings/{slugOrId}/gallery  # Get listing gallery images
GET    /api/v1/listings/categories          # Get all listing categories
GET    /api/v1/listings/categories/autocomplete  # Search categories
GET    /api/v1/listings/tags/autocomplete   # Search tags

Authenticated Vendor Endpoints

http
PATCH  /api/v1/listings/{listingId}/media/{mediaId}  # Update gallery media (order, collection)

Search Parameters

Text Search:

  • query - Full-text search across title, description, vendor name, location

Geographic Search:

  • google_place_id - Google Place ID for region-based search (any overlap logic)
  • latitude + longitude - Specific coordinates (point-in-circle logic)
  • radius_km - Search radius in kilometers (optional, for future use)

Filters:

  • categories - Comma-separated category names
  • tags - Comma-separated tag names
  • verified - Boolean filter for verified listings
  • featured - Boolean filter for featured listings
  • min_price - Minimum price filter
  • max_price - Maximum price filter

Sorting:

  • order_by - Sort field (price, rating, review_count, created_at, published_at)
  • order_dir - Sort direction (asc, desc)

Pagination:

  • per_page - Results per page (default: 15, max: 100)

Search Response Format

See API Documentation for details.

Search Region Metadata (when geographic search is used):

  • type - "circle" for Google Place ID searches, "point" for lat/lng searches
  • center - Search center point { lat, lng }
  • radius_miles - Search radius in miles (only for Google Place ID searches)
  • source - "google_place_id" or "coordinates"

This metadata enables the frontend to visualize the search area on a map.

Show Listing Endpoint

GET /api/v1/listings/{slugOrId}

Get detailed information about a specific listing by slug or UUID.

Parameters:

  • slugOrId - Listing slug (e.g., "premium-wedding-photography") or UUID

Response Format:

json
{
  "data": {
    "id": "uuid",
    "vendor_id": "uuid",
    "title": "Premium Wedding Photography",
    "slug": "premium-wedding-photography",
    "description": "Full day coverage...",
    "excerpt": "Professional wedding photography...",
    "price": 2500.00,
    "currency": "GBP",
    "location": "London",
    "based_in": "London, UK",
    "latitude": 51.5074,
    "longitude": -0.1278,
    "service_radius_miles": 50,
    "status": "published",
    "verified": true,
    "featured": false,
    "rating": 4.8,
    "review_count": 42,
    "view_count": 1250,
    "years_experience": 10,
    "package_inclusions": [...],
    "published_at": "2024-01-15T10:00:00Z",
    "created_at": "2024-01-10T10:00:00Z",
    "updated_at": "2024-01-15T10:00:00Z",
    "vendor": {
      "id": "uuid",
      "business_name": "ABC Photography",
      "slug": "abc-photography"
    },
    "categories": [...],
    "tags": [...],
    "featured_image": {
      "id": "uuid",
      "url": "https://...",
      "filename": "featured.jpg",
      "title": "Featured Image",
      "alt_text": "Wedding photography"
    }
  }
}

Note: Only returns listings with status = 'published'.

GET /api/v1/listings/{slugOrId}/gallery

Returns all gallery images for a listing.

Response Format:

json
{
  "data": [
    {
      "id": "uuid",
      "path": "listings/gallery/image.jpg",
      "filename": "image.jpg",
      "mime_type": "image/jpeg",
      "size": 1024000,
      "title": "Wedding ceremony setup",
      "alt_text": "Beautiful outdoor ceremony",
      "url": "https://storage.example.com/listings/gallery/image.jpg",
      "collection": "gallery",
      "order": 1
    }
  ]
}

PATCH /api/v1/listings/{listingId}/media/{mediaId}

Update gallery media order or collection. Requires vendor authentication and ownership.

Request Body:

json
{
  "order": 2,
  "collection": "gallery"
}

Response:

json
{
  "message": "Media updated successfully",
  "data": {
    "id": "uuid",
    "path": "listings/gallery/image.jpg",
    "filename": "image.jpg",
    "mime_type": "image/jpeg",
    "size": 1024000,
    "title": "Wedding ceremony setup",
    "alt_text": "Beautiful outdoor ceremony",
    "url": "https://storage.example.com/listings/gallery/image.jpg",
    "collection": "gallery",
    "order": 2
  }
}

Categories Endpoints

GET /api/v1/listings/categories

Get all listing categories.

Response Format:

json
{
  "data": [
    {
      "id": "uuid",
      "slug": "photography",
      "label": "Photography"
    },
    {
      "id": "uuid",
      "slug": "videography",
      "label": "Videography"
    }
  ]
}

GET /api/v1/listings/categories/autocomplete?query={search}

Search categories by name for autocomplete functionality.

Parameters:

  • query - Search term (optional)

Response Format:

json
[
  {
    "id": "uuid",
    "slug": "photography",
    "label": "Photography"
  }
]

Tags Endpoint

GET /api/v1/listings/tags/autocomplete?query={search}

Search tags by name for autocomplete functionality.

Parameters:

  • query - Search term (optional)

Response Format:

json
[
  {
    "id": "uuid",
    "slug": "outdoor-wedding",
    "label": "Outdoor Wedding"
  }
]

Geographic Search Architecture

Two-Step Search Process

The listing search uses a sophisticated two-step process combining PostGIS spatial queries with Typesense full-text search:

  1. PostGIS filters listings by geographic area (PostgreSQL)

    • Google Places: overlapsWithRegion(centerLat, centerLng, radiusMiles) - "any overlap" logic
    • Lat/Lng: withinServiceAreaOf(latitude, longitude) - "point-in-circle" logic
    • Returns array of listing IDs that match geographic criteria
  2. Typesense performs full-text search within filtered IDs

    • Applies non-geographic filters (categories, tags, price, etc.)
    • ID filter restricts search to PostGIS-filtered listings
    • Provides fast full-text search and faceting

PostGIS Spatial Queries

Google Place ID Search (Any Overlap Logic):

sql
-- Find listings whose service area overlaps with the Google Place region
-- Overlap occurs when: distance_between_centers <= listing_radius + region_radius
ST_DWithin(
    service_center::geography,
    ST_SetSRID(ST_MakePoint(region_lng, region_lat), 4326)::geography,
    (service_radius_miles + region_radius_miles) * 1609.34  -- Convert miles to meters
)

Lat/Lng Search (Point-in-Circle Logic):

sql
-- Find listings whose service area contains the search point
ST_DWithin(
    service_center::geography,
    ST_SetSRID(ST_MakePoint(search_lng, search_lat), 4326)::geography,
    service_radius_miles * 1609.34  -- Convert miles to meters
)

Model Scopes

The Listing model provides convenient scopes for geographic queries:

php
// Point-in-circle: Find listings that service a specific location
Listing::withinServiceAreaOf(51.5074, -0.1278)->get();

// Any overlap: Find listings that overlap with a region
Listing::overlapsWithRegion(51.5074, -0.1278, 10.5)->get();

// Multi-location: Find listings that service ALL locations
Listing::coversAllLocations([
    ['latitude' => 51.5074, 'longitude' => -0.1278],  // Ceremony
    ['latitude' => 51.5155, 'longitude' => -0.1426],  // Reception
])->get();

Database Schema

listings table

Key Fields:

  • id - UUID primary key
  • vendor_id - Foreign key to vendors table
  • title, slug - Listing title and URL slug
  • description, excerpt - Content fields
  • price, price_extras_description, max_hours, currency - Pricing
  • latitude, longitude - Listing location coordinates
  • service_center - PostGIS geography(Point, 4326) for spatial queries
  • service_radius_miles - Service area radius
  • travel_included_miles - Free travel distance
  • coverage_min_lat, coverage_min_lng, coverage_max_lat, coverage_max_lng - Legacy rectangular bounds (preserved for regression testing)
  • status - ENUM(draft, pending, published, archived)
  • verified, featured - Boolean flags
  • rating, review_count, view_count - Metrics
  • years_experience - Provider experience
  • featured_image_id - Foreign key to media table
  • package_inclusions - JSON array
  • external_reviews - JSON array
  • SEO fields: meta_title, meta_description, focus_keyword, canonical_url
  • Open Graph fields: og_title, og_description, og_image_url
  • Twitter Card fields: twitter_title, twitter_description, twitter_image_url
  • Booking configuration: calendar_sync_url, max_booking_days, min_booking_days, booking_lead_time, booking_window_days, requires_approval
  • published_at, created_at, updated_at, deleted_at
  • wp_id - WordPress migration tracking

Indexes:

  • Primary key on id
  • Foreign key on vendor_id
  • Unique index on slug
  • Unique index on wp_id
  • Composite index on [status, published_at]
  • Composite index on [vendor_id, status]
  • Composite index on [latitude, longitude]
  • GIST spatial index on service_center (PostGIS)

categories table

Listing categories (e.g., "Wedding Photographers", "Wedding Venues").

Pivot: categorizables (polymorphic many-to-many)

tags table

Listing tags for search and filtering.

Pivot: taggables (polymorphic many-to-many)

Relationships

Belongs To

  • Vendor - Each listing belongs to one vendor

Has Many

  • Reviews - Listing can have reviews
  • Bookings - Listing receives booking requests
  • Favourites - Users can favourite listings

Many to Many

  • Categories - Listing can belong to multiple categories
  • Tags - Listing has searchable tags
  • Media - Listing has images, videos, documents

Integration Points

Search Domain (Typesense)

Listings are indexed in Typesense for fast full-text search and faceted filtering.

Searchable Fields:

php
public function toSearchableArray(): array
{
    return [
        'id' => (string) $this->id,
        'title' => $this->title,
        'description' => $this->description ?? '',
        'price' => $this->price ? (float) $this->price : 0.0,
        'vendor_id' => (string) $this->vendor_id,
        'vendor_name' => $this->vendor?->business_name ?? '',
        'location' => $this->location ?? '',
        'based_in' => $this->based_in ?? '',
        'status' => $this->status,
        'verified' => (bool) $this->verified,
        'featured' => (bool) $this->featured,
        'rating' => $this->rating ? (float) $this->rating : 0.0,
        'review_count' => (int) $this->review_count,
        'view_count' => (int) $this->view_count,
        'categories' => $this->categories->pluck('name')->toArray(),
        'tags' => $this->tags->pluck('name')->toArray(),
        'published_at' => $this->published_at?->timestamp,
        'created_at' => $this->created_at->timestamp,
    ];
}

Note: Geographic fields (latitude, longitude, service_center, coverage bounds) are not indexed in Typesense. Geographic filtering is handled by PostGIS before Typesense search.

Search Features:

  • Full-text search across title, description, vendor name, location
  • Faceted filtering by categories, tags, price, verified, featured
  • Sorting by price, rating, review_count, created_at, published_at
  • Auto-sync on model create/update/delete via Laravel Scout

Media Domain

Listings use the polymorphic media system with the following collections:

  • featured_image - Primary listing image (single)
  • gallery - Additional listing images (multiple, ordered)

Relationship:

php
$listing->media()->wherePivot('collection', 'gallery')->get();
$listing->featuredImage; // BelongsTo relationship via featured_image_id

Vendors Domain

  • Each listing belongs to one vendor
  • Vendor status affects listing visibility
  • Vendor verification required for verified listings
  • Vendor's business_name is indexed in Typesense for search

Google Places API

  • Converts Google Place IDs to viewport bounds (northeast/southwest corners)
  • Calculates center point and radius using Haversine formula
  • Validates Place IDs are within allowed countries (UK only currently)
  • Used for region-based geographic searches

Testing Strategy

Test Coverage

Location: modules/Listing/Tests/

Feature Tests (modules/Listing/Tests/Feature/ListingPublicApiTest.php):

  • ✅ 61 tests covering all search scenarios
  • Geographic search (Google Place ID and lat/lng)
  • Text search with Typesense
  • Category and tag filtering
  • Price range filtering
  • Verified and featured filters
  • Pagination and sorting
  • Search region metadata
  • Geographic restrictions (UK only)
  • Edge cases and error handling

Key Test Examples:

php
it('returns search region metadata for Google Place ID searches', function () {
    // ... test implementation
    expect($response->json('meta.search_region.type'))->toBe('circle');
    expect($response->json('meta.search_region.radius_miles'))->toBe(10.5);
});

it('performs geographic search by Google Place ID (PostGIS any overlap)', function () {
    // ... test implementation
    $response->assertSuccessful();
});

it('only returns published listings', function () {
    // ... test implementation
    expect($response->json('data'))->each->toHaveKey('status', 'published');
});

Running Tests

bash
# Run all listing tests
docker compose exec -T wedissimo-api vendor/bin/pest modules/Listing/Tests/

# Run specific test file
docker compose exec -T wedissimo-api vendor/bin/pest modules/Listing/Tests/Feature/ListingPublicApiTest.php

# Run specific test
docker compose exec -T wedissimo-api vendor/bin/pest --filter="returns search region metadata"

Livewire Components

ListingSearch Component

Location: modules/Listing/Livewire/ListingSearch.php

Features:

  • Real-time search with debouncing and throttling
  • Category and tag autocomplete
  • Price range filtering
  • Verified/featured filters
  • Geographic search (Google Places integration)
  • Sortable columns
  • Pagination
  • Query string persistence
  • Loading states

CategoryAutocomplete Component

Location: modules/Listing/Livewire/CategoryAutocomplete.php

Features:

  • Real-time category search
  • Keyboard navigation
  • Multi-select support

TagAutocomplete Component

Location: modules/Listing/Livewire/TagAutocomplete.php

Features:

  • Real-time tag search
  • Keyboard navigation
  • Multi-select support

ListingEdit Component

Location: modules/Listing/Livewire/ListingEdit.php

Features:

  • Listing creation and editing
  • Media upload and management
  • Category and tag assignment
  • SEO field management
  • Geographic location selection
  • Service radius configuration

Key Features

Search Region Metadata

When performing geographic searches, the API returns metadata about the search area that can be used to visualize the search on a map.

Use Case: QA and users can verify that listings are correctly returned based on their service area coverage.

Example Scenario:

  • User searches for: London (lat: 51.5074, lng: -0.1278)
  • Provider location: Colchester (50 miles from London)
  • Provider service radius: 90 miles
  • Result: Provider appears in results because London is within their 90-mile service area

Frontend Visualization:

  1. Plot the search point using meta.search_region.center
  2. Plot the provider's location using listing's latitude/longitude
  3. Draw the provider's service circle using service_radius_miles
  4. Verify visually that the search point falls inside the provider's service area

Response Format:

json
{
  "meta": {
    "search_region": {
      "type": "point",
      "center": { "lat": 51.5074, "lng": -0.1278 },
      "source": "coordinates"
    }
  }
}

For Google Place ID searches, the response includes radius_miles:

json
{
  "meta": {
    "search_region": {
      "type": "circle",
      "center": { "lat": 51.5074, "lng": -0.1278 },
      "radius_miles": 10.5,
      "source": "google_place_id"
    }
  }
}

PostGIS Spatial Indexing

The module uses PostGIS for true circular service area calculations with Earth-accurate distance calculations (Haversine formula).

Benefits:

  • Accurate geographic searches (vs rectangular bounding boxes)
  • Efficient spatial indexing with GIST
  • Support for complex multi-location searches
  • "Any overlap" logic for region searches
  • "Point-in-circle" logic for specific location searches

Auto-Sync: The service_center geography point is automatically synced from latitude/longitude via model booted() method.

Calendar Sync & Availability

The Listing module includes a calendar synchronization system that imports blocked dates from external iCal feeds (Google Calendar, Airbnb, Outlook, etc.) to manage vendor availability.

How It Works

  1. Vendors can set a default_calendar_sync_url (iCal feed) at the vendor level
  2. Listings can have their own calendar_sync_url that overrides or supplements the vendor default
  3. A scheduled job fetches calendars and stores blocked dates per-listing
  4. Search API excludes listings that are blocked on the requested date

Calendar URL Resolution

When syncing a listing, the system collects calendar URLs using merge logic:

Listing URLVendor DefaultURLs Synced
SetSet (different)Both - merged
SetSet (same URL)One - deduplicated
SetNot setListing URL only
Not setSetVendor default only
Not setNot setNo sync

Key Point: When both listing and vendor have different calendar URLs, blocked dates from both calendars are merged. This allows:

  • Vendor-level holidays and personal time off (vendor default calendar)
  • Listing-specific bookings (listing calendar)

Blocked Date Storage

Blocked dates are stored in the listing_availability table:

sql
listing_availability (
    id UUID PRIMARY KEY,
    listing_id UUID NOT NULL,       -- FK to listings
    blocked_date DATE NOT NULL,     -- The blocked day (full day, not time slot)
    event_uid VARCHAR(255),         -- Original iCal event UID
    event_summary VARCHAR(255),     -- Event title from calendar
    sync_hash VARCHAR(64),          -- For change detection (reserved)
    synced_at TIMESTAMP,            -- When this record was last synced
    UNIQUE (listing_id, blocked_date)
)

Important: The system blocks entire days, not time slots. A 1-hour event on June 15th blocks the entire day.

Incremental Sync Optimization

To avoid unnecessary database writes, the sync uses two-level change detection:

  1. HTTP ETag Header

    • Sends If-None-Match header with stored ETag
    • If server returns 304 Not Modified, skips sync entirely
    • Stored per-listing in listings.calendar_feed_etag
  2. iCal LAST-MODIFIED Property

    • Extracts max LAST-MODIFIED from all events
    • Compares to stored listings.calendar_last_modified
    • Skips sync if unchanged

Sync Decision Flow:

1. Fetch calendar with If-None-Match header
2. If 304 response → Update timestamp only, done
3. If 200 response → Parse iCal content
4. If LAST-MODIFIED unchanged → Update timestamp only, done
5. Otherwise → Full sync (DELETE + INSERT all dates)

Listing Model Fields

php
// Calendar sync configuration
'calendar_sync_url'          // Listing-specific iCal URL (optional)
'calendar_feed_etag'         // Stored ETag for conditional requests
'calendar_last_modified'     // Max LAST-MODIFIED from last sync
'calendar_last_synced_at'    // Timestamp of last sync attempt

Services & Jobs

ICalParserService (modules/Listing/Services/ICalParserService.php)

php
// Fetch and parse with conditional request support
$result = $parser->fetchAndParse($url, $previousEtag);
// Returns: ['dates' => [...], 'etag' => '...', 'last_modified' => Carbon]
// Returns null if 304 Not Modified

// Backward-compatible simple method
$dates = $parser->getBlockedDateMap($url);

ListingAvailabilityService (modules/Listing/Services/ListingAvailabilityService.php)

php
// Sync blocked dates for a listing (full replace)
$service->syncListingAvailability($listingId, $dateMap);

SyncListingCalendarJob (modules/Listing/Jobs/SyncListingCalendarJob.php)

php
// Dispatch sync for a listing
SyncListingCalendarJob::dispatch($listingId);

// Force full sync (ignore ETag/LAST-MODIFIED)
SyncListingCalendarJob::dispatch($listingId, forceSync: true);

SyncAllListingCalendarsCommand (modules/Listing/Console/SyncAllListingCalendarsCommand.php)

bash
# Sync all published listings with calendar URLs
php artisan calendar:sync-listings

# Sync for specific vendor
php artisan calendar:sync-listings --vendor-id=uuid

# Limit number of jobs dispatched
php artisan calendar:sync-listings --limit=100

Search Integration

When searching with a date parameter, blocked listings are excluded:

php
// ListingService.php
if (isset($filters['date'])) {
    $query->whereDoesntHave('availability', function ($q) use ($date) {
        $q->where('blocked_date', $date);
    });
}

API Usage:

http
GET /api/v1/listings/search?date=2025-12-25

Returns only listings that are not blocked on December 25, 2025.

Configuration

php
// config/listing.php (or module config)
'calendar' => [
    'sync_interval_minutes' => env('CALENDAR_SYNC_INTERVAL_MINUTES', 15),
    'sync_queue' => env('CALENDAR_SYNC_QUEUE', 'calendar-sync'),
    'sync_horizon_years' => env('CALENDAR_SYNC_HORIZON_YEARS', 2),
],

Testing

bash
# Run calendar sync tests
docker compose exec -T wedissimo-api vendor/bin/pest modules/Listing/Tests/Feature/SyncListingCalendarJobTest.php
docker compose exec -T wedissimo-api vendor/bin/pest modules/Listing/Tests/Unit/ICalParserServiceTest.php

Test Coverage:

  • Conditional sync (304 Not Modified)
  • LAST-MODIFIED change detection
  • Force sync option
  • Vendor default fallback
  • Calendar merge (listing + vendor)
  • URL deduplication
  • Missing calendar handling

Limitations

  • Full-day blocking only: Events block entire days, not specific time slots
  • No recurring event optimization: Each occurrence of a recurring event creates a separate blocked date record
  • Sync horizon: Only syncs events within configured horizon (default: 2 years)

Future Enhancements

  • Time-slot blocking (instead of full-day)
  • Real-time booking confirmation with fresh iCal fetch
  • Webhook support for instant calendar updates
  • Calendar export (generate iCal feed for listings)

Future Enhancements

Domain Events (Planned)

The following domain events are planned but not yet implemented:

  • ListingCreated - Dispatched when a new listing is created
  • ListingPublished - Dispatched when listing status changes to published
  • ListingFeatured - Dispatched when listing is promoted to featured
  • ListingArchived - Dispatched when listing is archived
  • ListingUpdated - Dispatched when listing details are modified

These events will enable:

  • Automatic search index updates
  • Vendor notifications
  • Analytics tracking
  • Integration with other domains (Bookings, Communications, etc.)

Feature Enhancements

  • Multi-location search UI (ceremony + reception + accommodation)
  • Distance-based sorting (nearest first)
  • Service area visualization on listing detail pages
  • Heatmap of listing coverage density
  • Listing packages (Bronze, Silver, Gold tiers)
  • Dynamic pricing based on season/demand
  • Listing templates for quick creation
  • AI-powered description suggestions
  • Automated quality scoring
  • Competitor price analysis
  • Listing performance analytics
  • A/B testing for listing content
  • Multi-language listing support

Wedissimo API Documentation