Skip to content

Vendor Listings (SRS-LST)

Purpose

Owns the vendor's public storefront and its content management: profile page, media gallery and videos, bilingual descriptions, services/packages priced in KWD, availability calendar and working hours, service area, location, FAQs, cancellation policies, the listing quality score, and content moderation. Everything a customer sees on /{locale}/v/{vendor-slug} is specified here. Business identity/verification is owned by Vendor Onboarding; discovery/ranking of listings by Marketplace & Search; plan-based content limits by the plan matrix.

Actors

Actor Description
Vendor owner Edits all listing content, publishes, manages calendar.
Vendor manager/staff (Scale) Content editing per team-role permissions.
Guest / Customer Views the public profile; customers act on it (favorite, RFQ, message, book).
Admin (Moderator) Reviews flagged/queued content, approves or rejects with reasons.
System Image processing pipeline, quality-score recalculation, index sync.

User stories

  • As a vendor, I want to build a rich profile with photos, packages, and policies, so that customers trust me enough to request quotes.
  • As a vendor, I want to edit content in Arabic first and add English when I can, so that language is never a blocker to going live.
  • As a vendor, I want to block dates I'm fully booked, so that I stop receiving requests I must decline.
  • As a customer, I want package prices in KWD with what's included, so that I can budget before contacting anyone.
  • As a customer, I want to see the vendor's cancellation policy before booking, so that I know my risk.
  • As a moderator, I want a queue of new/edited listing content with diffs, so that nothing misleading or inappropriate goes public.
  • As a vendor, I want a quality score with concrete tips, so that I know exactly how to improve my visibility.

Functional requirements

Profile & descriptions

ID Requirement Phase
SRS-LST-001 Each approved vendor has exactly one listing (public profile) at /{locale}/v/{slug} containing: cover image, logo, business names AR/EN, verified badge, primary + secondary categories, rating summary, description, gallery, videos, packages, availability indicator, service area, location map, working hours, FAQs, policies, reviews (read from Experience domain), and CTAs (Request Quote, Message, Favorite). MVP
SRS-LST-002 Descriptions: description_ar and/or description_en, 100–3000 chars each, basic rich text only (paragraphs, bold, bullet lists — no links, no HTML injection). At least one language is required for publication; the profile shows a language-fallback notice when only one exists. MVP
SRS-LST-003 Contact details (phone/email/social) are never shown on the public listing; all contact flows through platform CTAs. Vendor-entered description text is scanned for phone numbers/URLs/handles and such content is flagged to moderation (business rule 6). MVP
SRS-LST-004 Slug auto-generated from business_name_en (transliterated, unique, ≤ 60 chars); vendor may customize once per 90 days; old slugs 301-redirect. MVP
ID Requirement Phase
SRS-LST-010 Image gallery with drag-and-drop ordering. Formats: JPEG, PNG, WebP. Per file ≤ 10 MB, min 800×600 px. Count limited by plan (Free 5 / Basic 15 / Professional 40 / Enterprise 100). First image = cover unless overridden. MVP
SRS-LST-011 Upload pipeline: virus scan → EXIF strip (incl. GPS) → generate WebP renditions (thumb 400 w, card 800 w, full 1920 w) → serve via CDN with lazy loading. Originals retained in private storage. MVP
SRS-LST-012 Every image accepts optional bilingual captions (caption_ar/caption_en, ≤ 150 chars) and alt text auto-defaults to "{business name} — {category}" for accessibility (WCAG). MVP
SRS-LST-013 Videos: embedded YouTube/Vimeo links at MVP (count limited by plan: 0/1/5/15). Link validated (provider oEmbed check), rendered as facade thumbnails (no third-party JS until tap). Direct video uploads Scale
SRS-LST-014 360° images and AR venue preview Vision

Services & packages

ID Requirement Phase
SRS-LST-020 Vendors define packages (the bookable/quotable units): name_ar/name_en, description_ar/description_en (≤ 1000 chars), price (KWD, 3 decimals), price_type (fixed, starting_from, per_person, per_hour), min_quantity/max_quantity where applicable (e.g. per-person catering min 50), inclusions list (repeatable bilingual bullet items, ≤ 20), optional package image, status (active/hidden). Package count limited by plan (3/10/30/unlimited). MVP
SRS-LST-021 Prices are stored as DECIMAL(12,3) KWD; display always shows 3 decimals (e.g. KWD 1,250.500). starting_from and per_person types render the appropriate bilingual prefix ("يبدأ من / from", "للشخص / per person"). MVP
SRS-LST-022 The listing's "starting price" (used on marketplace cards and price sorting) = lowest active package price, recomputed on any package change. A vendor MAY publish with zero packages; the listing then shows "Request a quote" and is excluded from budget filtering (SRS-MKT business rule 3). MVP
SRS-LST-023 Package add-ons (optional extras with own prices, selectable in RFQs/bookings) Scale

Availability calendar & working hours

ID Requirement Phase
SRS-LST-030 Working hours: per weekday open/close times (multiple ranges per day allowed, e.g. 10:00–14:00 and 16:00–22:00), plus "closed" days. Kuwait weekend default suggestion Fri–Sat, fully editable. Displayed on the profile in the viewer's locale with a live "open now / يفتح الآن" indicator (vendor timezone). MVP
SRS-LST-040 Availability calendar: month view where the vendor marks dates as available (default), blocked, or booked (auto-set by confirmed bookings, not manually editable). Blocking supports single dates and ranges. Horizon: 24 months ahead. MVP
SRS-LST-041 The public profile shows availability status for a customer-selected date (available / unavailable), never the vendor's reasons or other customers' bookings. The calendar feeds the marketplace availability filter (SRS-MKT-021). MVP
SRS-LST-042 Calendar changes propagate to the search index within 60 s. MVP
SRS-LST-043 Google Calendar two-way sync; per-package capacity (multiple bookings per date) Scale

Service area & location

ID Requirement Phase
SRS-LST-050 Service area: multi-select of governorates/areas the vendor serves (Kuwait launch: 6 governorates with PACI areas), or "all Kuwait". Venue-type vendors instead emphasize their fixed location. Displayed as chips on the profile and used by area filtering. MVP
SRS-LST-051 Location: Google Maps pin (from onboarding, editable) with embedded map on the profile and a "Directions" deep link. Vendors MAY hide the exact pin (show area only) — venues cannot hide it. MVP

FAQs & policies

ID Requirement Phase
SRS-LST-052 FAQs: up to 15 Q&A pairs, bilingual capable (question_ar/en ≤ 200 chars, answer_ar/en ≤ 1000 chars), vendor-ordered, collapsible on the profile. MVP
SRS-LST-053 Cancellation policy: vendor selects exactly one platform template — Flexible (free cancellation ≥ 7 days before event date; 50 % deposit refund 3–6 days; none < 3 days), Moderate (free ≥ 14 days; 50 % 7–13 days; none < 7 days), Strict (free ≥ 30 days; 25 % 14–29 days; none < 14 days). Templates are platform-authored bilingual legal text; vendors cannot write custom policies at MVP (custom = Scale, admin-reviewed). The selected policy is displayed on the profile and snapshotted onto every booking at confirmation time. MVP
SRS-LST-054 Additional policy toggles: deposit percentage required (25 % / 50 %, default 25 %), "outdoor events" yes/no, other structured attributes per category (data-driven attribute sets). MVP

Quality score & moderation

ID Requirement Phase
SRS-LST-055 Listing quality score 0–100, recomputed on content change, visible only to the vendor and admins with an actionable checklist. Weights: both-language descriptions 15, ≥ 8 gallery images 20, ≥ 1 video 5, ≥ 3 packages with prices 20, calendar maintained (updated < 30 days) 10, working hours set 5, FAQs ≥ 3 10, policy + deposit configured 5, response rate to RFQs ≥ 80 % 10. Score contributes to search boost_score (SRS-MKT-026). MVP
SRS-LST-060 Moderation: first publication requires full review; subsequent edits to text and media enter a moderation queue but the previous approved version stays live until the edit is approved (versioned content). Price, calendar, working-hours, and ordering changes apply instantly without review. MVP
SRS-LST-061 Moderator actions: approve, reject (reason from a taxonomy: inappropriate content, misleading claims, contact-info leakage, copyright, quality below floor, other + comment). Rejections notify the vendor with the exact reason and the offending field highlighted. MVP
SRS-LST-062 Auto-checks before human review: image nudity/violence classification, contact-info regex scan (phones, URLs, @handles) on all text fields, duplicate-image hash detection across other vendors (copyright signal). Auto-flags annotate the moderation item; they do not auto-reject at MVP. MVP
SRS-LST-063 Admin can unpublish any live listing immediately (severe violation), with mandatory reason, audit log, and vendor notification; unpublish removes it from search within 60 s. MVP

Business rules

  1. One vendor = one listing. Branch-specific listing views are Scale (with multi-branch).
  2. Plan downgrade never deletes content: over-limit images/videos/packages are hidden lowest-recently-added-first and restored on upgrade (SRS-VOS-043).
  3. A listing is searchable only when: vendor approved + subscription state not suspended + listing status Published (SRS-MKT business rule 1).
  4. Confirmed bookings automatically mark the date "booked" on the calendar for venue-type packages (single-event-per-day categories); other categories default to manual management at MVP.
  5. Cancellation policy changes apply only to future bookings; existing bookings keep their snapshotted policy.
  6. Contact-information leakage (phone numbers, external links, social handles) in any public-facing text is a moderation violation: first offense = rejection with warning, repeated offenses escalate to vendor suspension review.
  7. The moderation SLA target is 1 business day for edits, 2 for first publication; queue age is an admin KPI.
  8. All moderation decisions, unpublish actions, and policy template changes are audit-logged.

Validation rules

Field Rule
description_ar / description_en 100–3000 chars; sanitized rich text (whitelist: p, strong, ul/li); no URLs/emails/phone patterns (flag on match).
Gallery image JPEG/PNG/WebP; ≤ 10 MB; ≥ 800×600 px; count ≤ plan limit.
caption_ar/en ≤ 150 chars.
Video URL Valid YouTube/Vimeo URL resolving via oEmbed; count ≤ plan limit.
Package name_ar/en 3–80 chars.
Package price 0.500 ≤ price ≤ 999999.999 KWD, exactly 3 decimals stored.
price_type Enum: fixed, starting_from, per_person, per_hour.
Inclusions 1–20 items, each ≤ 120 chars.
Working hours Valid HH:MM ranges, no overlaps within a day, ≤ 3 ranges/day.
Blocked-date range start ≤ end; within today … today + 24 months; cannot block a date with a confirmed booking (must cancel via Booking module first).
Service area ≥ 1 selection from areas reference or all_kuwait.
FAQ ≤ 15 pairs; question ≤ 200, answer ≤ 1000 chars.
Slug ^[a-z0-9]+(-[a-z0-9]+)*$, 3–60 chars, unique, change ≤ 1 / 90 days.
Deposit percentage Enum: 25, 50.

Permissions matrix

Action Guest/Customer Vendor owner Vendor manager (Scale) Vendor staff (Scale) Admin Moderator Admin Super
View published listing
View draft/pending content & quality score
Edit content (media, text, packages, FAQs)
Manage calendar & working hours
Submit for publication
Approve / reject content
Unpublish live listing
Change policy templates (platform text)

Status / state machine

stateDiagram-v2
    [*] --> Draft: vendor approved (VOS), listing shell created
    Draft --> PendingReview: vendor submits for publication
    PendingReview --> Published: moderator approves
    PendingReview --> Draft: moderator rejects (reasons attached)
    Published --> PendingReview: text/media edit submitted (previous version stays live)
    Published --> Unpublished: admin unpublish OR vendor hides listing
    Unpublished --> PendingReview: vendor resubmits / admin requires re-review
    Unpublished --> Published: vendor re-shows (if unpublish was vendor-initiated)

Package-level status is a simple active/hidden flag inside a Published listing; media items carry pending/approved/rejected within the content-version model.

Edge cases

  • Edit pending while another edit is made: single open version per listing — new edits amend the same pending version; moderators always review the latest snapshot with a diff against live.
  • Vendor blocks a date that has a pending (not confirmed) booking request: allowed, but the vendor is warned and the pending request is flagged to the customer.
  • Plan limit reached mid-upload of a multi-file batch: files beyond the limit are rejected individually with a per-file message; accepted files remain.
  • oEmbed provider outage during video validation: accept provisionally, mark unverified, re-validate via nightly job; unverifiable links after 7 days are hidden and the vendor notified.
  • Two staff edit the calendar simultaneously (Scale): last-write-wins per date cell with change history; conflicting booking-block collisions resolved in favor of the booking.
  • Arabic-only listing viewed in English: EN UI renders AR content with dir="rtl" islands and a "content available in Arabic" notice — never machine-translated silently at MVP.
  • Quality score gaming (e.g. 8 near-duplicate images): duplicate-hash detection collapses near-duplicates for scoring purposes.
  • Vendor suspended by admin: listing auto-unpublishes (state Unpublished, reason vendor_suspended), restored to its prior state on reinstatement.
  • Slug change: old slug 301s for ≥ 12 months; RFQ/booking deep links use ULIDs and are unaffected.

Empty / loading / error states

Screen Empty Loading Error
Public profile (customer) Sections with no content collapse entirely (no "no FAQs" placeholders); zero-package listings show the quote CTA block Skeleton: cover, title bar, gallery placeholders; images lazy-load with LQIP blur Partial-failure tolerant: failed gallery images show retry tiles; whole-profile failure shows branded error page with search-back CTA
Listing editor (vendor) First-run checklist wizard ("Add photos → Add a package → Set your hours → Submit") mirroring the quality-score items Autosave indicator; per-section skeletons Field-level bilingual errors; failed autosave banner with manual retry; uploads resumable
Calendar editor Current month, all dates default-available, legend explained Month-grid skeleton Failed date toggle rolls back optimistically with toast
Moderation queue (admin) "Queue clear" + SLA stats Skeleton rows with diff placeholders Decision failures preserve typed comments; state-conflict prompts refresh (optimistic locking)

Acceptance criteria

  • A newly approved vendor can complete Draft → PendingReview → Published; the listing appears in search within 60 s of approval.
  • Gallery enforces plan-based counts, format/size/dimension rules, EXIF-stripping (verified: uploaded image with GPS EXIF serves without GPS data), and CDN renditions.
  • A package priced 250.500 KWD displays as KWD 250.500 (EN) and د.ك 250.500 (AR) everywhere it appears, including marketplace cards.
  • Starting price recomputes when the cheapest package is hidden.
  • Blocking 2026-09-10 on the calendar removes the vendor from marketplace availability-filtered results for that date within 60 s; the "booked" state from a confirmed booking cannot be manually cleared.
  • Policy template text renders in both languages; a booking confirmed under Flexible keeps Flexible after the vendor switches to Strict.
  • An edit to a live listing keeps the old version public until approval; rejection restores the editor with reasons and highlighted fields.
  • Description containing "call me 99XXXXXX" is auto-flagged and cannot pass to Published without moderator override.
  • Quality score matches the documented weights for at least 5 fixture listings; checklist links deep-link to the right editor section.
  • Admin unpublish removes the listing from profile URL (branded unavailable page) and search within 60 s, and is audit-logged.

Data entities touched

listings, listing_versions (moderation snapshots), listing_media (images/videos, renditions), packages, package_inclusions, listing_faqs, listing_policies (template selection + deposit config), cancellation_policy_templates (platform), working_hours, calendar_dates (blocked/booked), service_areas (pivot to areas), listing_quality_scores, moderation_items, moderation_decisions, audit_logs; reads vendors, subscription state, reviews summary.

GET    /api/v1/vendors/{slugOrId}/listing                 (public)
GET    /api/v1/vendors/{vendorId}/listing/draft           (vendor)
PATCH  /api/v1/vendors/{vendorId}/listing
POST   /api/v1/vendors/{vendorId}/listing/media
PATCH  /api/v1/vendors/{vendorId}/listing/media/order
DELETE /api/v1/vendors/{vendorId}/listing/media/{mediaId}
POST   /api/v1/vendors/{vendorId}/packages
PATCH  /api/v1/vendors/{vendorId}/packages/{packageId}
DELETE /api/v1/vendors/{vendorId}/packages/{packageId}
PUT    /api/v1/vendors/{vendorId}/working-hours
GET    /api/v1/vendors/{vendorId}/calendar?month=YYYY-MM
PUT    /api/v1/vendors/{vendorId}/calendar/dates          (block/unblock)
PUT    /api/v1/vendors/{vendorId}/service-areas
PUT    /api/v1/vendors/{vendorId}/listing/faqs
PUT    /api/v1/vendors/{vendorId}/listing/policies
POST   /api/v1/vendors/{vendorId}/listing/submit          (for review)
GET    /api/v1/vendors/{vendorId}/listing/quality-score
GET    /api/v1/admin/moderation/listings
POST   /api/v1/admin/moderation/listings/{itemId}/decision
POST   /api/v1/admin/listings/{listingId}/unpublish