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 |
Gallery & videos
| 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
- One vendor = one listing. Branch-specific listing views are Scale (with multi-branch).
- Plan downgrade never deletes content: over-limit images/videos/packages are hidden lowest-recently-added-first and restored on upgrade (SRS-VOS-043).
- A listing is searchable only when: vendor approved + subscription state not suspended + listing status Published (SRS-MKT business rule 1).
- 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.
- Cancellation policy changes apply only to future bookings; existing bookings keep their snapshotted policy.
- 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.
- The moderation SLA target is 1 business day for edits, 2 for first publication; queue age is an admin KPI.
- 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
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