diff --git a/openapi.json b/openapi.json index 85a3960..d5fc2c7 100644 --- a/openapi.json +++ b/openapi.json @@ -556,13 +556,14 @@ "JWT-auth": [] } ], - "summary": "Get a public user profile; hosted events are included only for organizers", + "summary": "Get a public user profile; host stats are included only for verified hosts", "tags": ["Users"] } }, - "/api/v1/users/public/{id}/bootstrap": { + "/api/v1/users/public/{id}/events": { "get": { - "operationId": "UsersController_getPublicOrganizerBootstrap", + "description": "Paginated upcoming or held events. Non-verified profiles return an empty list.", + "operationId": "UsersController_listPublicOrganizerEvents", "parameters": [ { "name": "id", @@ -571,15 +572,94 @@ "schema": { "type": "string" } + }, + { + "name": "page", + "required": false, + "in": "query", + "description": "1-based page", + "schema": { + "minimum": 1, + "default": 1, + "type": "number" + } + }, + { + "name": "pageSize", + "required": false, + "in": "query", + "description": "Page size (alias accepted: limit)", + "schema": { + "minimum": 1, + "maximum": 100, + "default": 20, + "type": "number" + } + }, + { + "name": "sort", + "required": false, + "in": "query", + "description": "Sort field. Prefix with \"-\" for descending (e.g. \"-createdAt\").", + "schema": { + "example": "-createdAt", + "type": "string" + } + }, + { + "name": "filters", + "required": false, + "in": "query", + "description": "Column filters echoed back in the response. Values are strings; date ranges use \"from,to\".", + "schema": { + "additionalProperties": { + "type": "string" + }, + "type": "object" + } + }, + { + "name": "resultType", + "required": false, + "in": "query", + "description": "0 = JSON list (default). 1 = Excel export (returns response.fileData).", + "schema": { + "type": "number", + "enum": [0, 1] + } + }, + { + "name": "status", + "required": false, + "in": "query", + "schema": { + "default": "upcoming", + "type": "string", + "enum": ["upcoming", "held"] + } } ], "responses": { "200": { - "description": "", + "description": "Paginated list response", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PublicOrganizerBootstrapResponseDto" + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PublicOrganizerEventDto" + }, + "description": "Array of items for the current page." + }, + "response": { + "$ref": "#/components/schemas/PaginationMetaDto", + "description": "Pagination metadata for the current query." + } + }, + "required": ["items", "response"] } } } @@ -590,7 +670,7 @@ "JWT-auth": [] } ], - "summary": "Get the cacheable public user profile page bootstrap", + "summary": "List discoverable events for a verified public host", "tags": ["Users"] } }, @@ -628,44 +708,6 @@ "tags": ["Users"] } }, - "/api/v1/users/{id}/attendee-contact-links": { - "get": { - "description": "Returns non-public links only when the current user owns the organizer profile or has a confirmed booking for one of its events.", - "operationId": "UsersController_getAttendeeContactLinks", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PublicOrganizerContactLinkDto" - } - } - } - } - } - }, - "security": [ - { - "JWT-auth": [] - } - ], - "summary": "Get attendee-only contact links for an organizer", - "tags": ["Users"] - } - }, "/api/v1/users/me": { "get": { "description": "Returns the authenticated user profile and account capability fields.", @@ -807,6 +849,120 @@ "tags": ["Users"] } }, + "/api/v1/users/me/profile-summary": { + "get": { + "description": "Returns wallet balance, social/event/bookmark/notification counts, and short previews for the consumer profile dashboard.", + "operationId": "UsersController_getProfileSummary", + "parameters": [], + "responses": { + "200": { + "description": "Profile hub summary.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UserProfileSummaryResponseDto" + } + } + } + }, + "400": { + "description": "Validation error - request body, parameters, or query string are invalid", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "401": { + "description": "Unauthorized - invalid or missing JWT token", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "403": { + "description": "Forbidden - Insufficient permissions", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "404": { + "description": "Not Found - requested resource does not exist or is not visible", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + }, + "500": { + "description": "Internal server error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponseDto" + } + } + } + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get current user profile hub summary", + "tags": ["Users"] + } + }, + "/api/v1/users/{id}/attendee-contact-links": { + "get": { + "description": "Returns non-public links only when the current user owns the organizer profile or has a confirmed booking for one of its events.", + "operationId": "UsersController_getAttendeeContactLinks", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PublicOrganizerContactLinkDto" + } + } + } + } + } + }, + "security": [ + { + "JWT-auth": [] + } + ], + "summary": "Get attendee-only contact links for an organizer", + "tags": ["Users"] + } + }, "/api/v1/users/me/complete-profile": { "post": { "description": "Creates the user profile and activates a pending account. Only callable while status is pending.", @@ -938,14 +1094,17 @@ "name": "filters", "required": false, "in": "query", - "description": "Admin user filters via filters[key]=value (role, status, identityStatus, mobile, firstName, lastName, lastLoginAt, createdAt). Date ranges use from,to.", + "description": "Admin user filters via filters[key]=value (userType, role, status, identityStatus, mobile, firstName, lastName, gender, cityId, walletBalance, lastLoginAt, createdAt). userType is guest|host|admin. Date ranges and walletBalance use from,to.", "schema": { "additionalProperties": { "type": "string" }, "example": { - "role": "user", - "status": "active" + "userType": "host", + "status": "active", + "gender": "female", + "cityId": "1", + "walletBalance": "0,500000" }, "type": "object" } @@ -980,6 +1139,36 @@ "type": "string" } }, + { + "name": "filters[walletBalance]", + "required": false, + "in": "query", + "description": "Wallet balance range in Tomans as from,to.", + "schema": { + "example": "0,500000", + "type": "string" + } + }, + { + "name": "filters[cityId]", + "required": false, + "in": "query", + "description": "Filter by home city id.", + "schema": { + "example": "1", + "type": "string" + } + }, + { + "name": "filters[gender]", + "required": false, + "in": "query", + "description": "Filter by gender: male, female, or other.", + "schema": { + "example": "female", + "type": "string" + } + }, { "name": "filters[mobile]", "required": false, @@ -1014,11 +1203,21 @@ "name": "filters[role]", "required": false, "in": "query", - "description": "Filter by role: user or admin.", + "description": "Raw account role: user or admin. Prefer filters[userType] for guest/host/admin.", "schema": { "example": "user", "type": "string" } + }, + { + "name": "filters[userType]", + "required": false, + "in": "query", + "description": "Derived user type for admin triage: guest, host (identity verified), or admin.", + "schema": { + "example": "host", + "type": "string" + } } ], "responses": { @@ -1032,7 +1231,7 @@ "items": { "type": "array", "items": { - "$ref": "#/components/schemas/UserProfileResponseDto" + "$ref": "#/components/schemas/AdminUserListItemDto" }, "description": "Array of items for the current page." }, @@ -4914,7 +5113,7 @@ "name": "search", "required": false, "in": "query", - "description": "Search by guest first name, last name, full name, mobile, or booking code.", + "description": "Search by guest first name, last name, full name, or booking code. Mobile is never searchable for hosts.", "schema": { "maxLength": 100, "type": "string" @@ -9020,7 +9219,7 @@ } } }, - "summary": "Payment gateway callback for wallet deposits", + "summary": "Console/dev HMAC callback for wallet deposits (disabled when PAYMENT_GATEWAY_PROVIDER=jibit)", "tags": ["Financial"] } }, @@ -10841,7 +11040,7 @@ "schema": { "minimum": 1, "maximum": 100, - "default": 20, + "default": 10, "type": "number" } }, @@ -18580,47 +18779,6 @@ }, "required": ["channel", "label", "url", "value"] }, - "PublicOrganizerEventDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "format": "uuid" - }, - "slug": { - "type": "string" - }, - "title": { - "type": "string" - }, - "shortDescription": { - "type": "object", - "nullable": true - }, - "posterUrl": { - "type": "string", - "format": "uri", - "nullable": true - }, - "startsAt": { - "format": "date-time", - "type": "string" - }, - "cityName": { - "type": "string" - }, - "isFree": { - "type": "boolean" - }, - "price": { - "type": "number" - }, - "isPast": { - "type": "boolean" - } - }, - "required": ["id", "slug", "title", "posterUrl", "startsAt", "cityName", "isFree", "price", "isPast"] - }, "PublicOrganizerResponseDto": { "type": "object", "properties": { @@ -18662,10 +18820,12 @@ }, "followersCount": { "type": "number", - "minimum": 0 + "minimum": 0, + "description": "Follower count; 0 for non-verified guests." }, "isVerified": { - "type": "boolean" + "type": "boolean", + "description": "True when the user has a verified hosting identity." }, "memberSince": { "format": "date-time", @@ -18673,34 +18833,29 @@ }, "pastEventsCount": { "type": "number", - "minimum": 0 + "minimum": 0, + "description": "Discoverable held events count for verified hosts; 0 for guests." }, "totalGuestsCount": { "type": "number", "minimum": 0, - "description": "Confirmed bookings across all of the organizer’s events (unique seats / guests)." + "description": "Confirmed bookings across the host’s events; 0 for non-verified guests." }, "avgRating": { "type": "number", "nullable": true, - "description": "Mean published rating across the organizer’s events." + "description": "Mean published rating; null for guests or when unrated." }, "reviewsCount": { "type": "number", "minimum": 0, - "description": "Published review count across the organizer’s events." + "description": "Published review count; 0 for non-verified guests." }, "contactLinks": { "type": "array", "items": { "$ref": "#/components/schemas/PublicOrganizerContactLinkDto" } - }, - "events": { - "type": "array", - "items": { - "$ref": "#/components/schemas/PublicOrganizerEventDto" - } } }, "required": [ @@ -18713,169 +18868,156 @@ "pastEventsCount", "totalGuestsCount", "reviewsCount", - "contactLinks", - "events" + "contactLinks" ] }, - "ReviewStatus": { - "type": "string", - "enum": ["published", "hidden", "deleted"], - "description": "Publication status." - }, - "ReviewUserSummaryDto": { + "PublicOrganizerEventDto": { "type": "object", "properties": { "id": { "type": "string", - "description": "User identifier.", - "example": "c3cc67fb-8615-432c-9a57-a5249c4a6b1c", "format": "uuid" }, - "firstName": { - "type": "string", - "description": "User first name.", - "example": "Ali", + "slug": { + "type": "string" + }, + "title": { + "type": "string" + }, + "shortDescription": { + "type": "object", "nullable": true }, - "lastName": { + "posterUrl": { "type": "string", - "description": "User last name.", - "example": "Rezaei", + "format": "uri", "nullable": true }, - "avatarUrl": { - "type": "string", - "description": "Public profile avatar URL when the user has uploaded one.", - "example": "https://cdn.example.com/avatars/u1.jpg", - "nullable": true - } - }, - "required": ["id", "firstName", "lastName", "avatarUrl"] - }, - "OrganizerReviewResponseDto": { - "type": "object", - "properties": { - "id": { - "type": "string", - "description": "Review identifier.", - "example": "16e33f70-d3cc-492c-a8fa-e2317fbfb37b", - "format": "uuid" - }, - "eventId": { - "type": "string", - "description": "Reviewed event identifier.", - "example": "6f6d7d3a-90f2-4ad7-8d52-994a9676c5e1", - "format": "uuid" - }, - "userId": { - "type": "string", - "description": "Guest who wrote the review.", - "example": "c3cc67fb-8615-432c-9a57-a5249c4a6b1c", - "format": "uuid" - }, - "bookingId": { - "type": "string", - "description": "Booking that earned the right to review.", - "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", - "format": "uuid" - }, - "rating": { - "type": "number", - "description": "Star rating from 1 to 5.", - "example": 5, - "minimum": 1, - "maximum": 5 - }, - "body": { - "type": "string", - "description": "Optional review text; null when the guest rated without a comment.", - "example": "Great event — well organized and welcoming.", - "nullable": true - }, - "hostReplyBody": { - "type": "string", - "description": "Organizer reply text, when present.", - "example": "Thank you for attending!", - "nullable": true - }, - "hostReplyAt": { - "type": "string", - "description": "Timestamp when the organizer first replied.", - "example": "2026-08-15T09:00:00.000Z", + "startsAt": { "format": "date-time", - "nullable": true + "type": "string" }, - "status": { - "description": "Publication status.", - "example": "published", - "allOf": [ - { - "$ref": "#/components/schemas/ReviewStatus" - } - ] + "endsAt": { + "format": "date-time", + "type": "string" }, - "createdAt": { - "type": "string", - "description": "Review creation timestamp.", - "example": "2026-08-14T09:00:00.000Z", - "format": "date-time" + "cityName": { + "type": "string" }, - "updatedAt": { - "type": "string", - "description": "Last update timestamp.", - "example": "2026-08-15T09:00:00.000Z", - "format": "date-time" + "isFree": { + "type": "boolean" }, - "user": { - "description": "Guest who wrote the review (public name and optional avatar).", - "allOf": [ - { - "$ref": "#/components/schemas/ReviewUserSummaryDto" - } - ] + "price": { + "type": "number" }, - "eventTitle": { - "type": "string", - "description": "Event title for organizer-wide review lists." - }, - "eventSlug": { - "type": "string", - "description": "Event slug for linking to the public event page." + "isPast": { + "type": "boolean" } }, - "required": [ - "id", - "eventId", - "userId", - "bookingId", - "rating", - "body", - "status", - "createdAt", - "updatedAt", - "user", - "eventTitle", - "eventSlug" - ] + "required": ["id", "slug", "title", "posterUrl", "startsAt", "endsAt", "cityName", "isFree", "price", "isPast"] }, - "PublicOrganizerBootstrapResponseDto": { + "ListResultType": { + "type": "number", + "enum": [0, 1], + "description": "Result type requested by the client: 0 for JSON list, 1 for Excel export." + }, + "PaginationMetaDto": { "type": "object", "properties": { - "organizer": { - "$ref": "#/components/schemas/PublicOrganizerResponseDto" + "page": { + "type": "number", + "description": "Current page number using a 1-based index.", + "example": 1, + "minimum": 1 }, - "reviews": { - "type": "array", - "items": { - "$ref": "#/components/schemas/OrganizerReviewResponseDto" + "pageSize": { + "type": "number", + "description": "Number of items requested per page.", + "example": 20, + "minimum": 1, + "maximum": 100 + }, + "totalItemsCount": { + "type": "number", + "description": "Total number of items matching the current query.", + "example": 42, + "minimum": 0 + }, + "totalPages": { + "type": "number", + "description": "Total number of pages available for the current query.", + "example": 3, + "minimum": 0 + }, + "sort": { + "type": "string", + "description": "Applied sort expression. Prefix the field with - for descending order.", + "example": "-createdAt", + "minLength": 1, + "maxLength": 100 + }, + "filters": { + "type": "object", + "description": "Whitelisted filters applied to the list endpoint.", + "example": { + "status": "pending" + }, + "additionalProperties": { + "type": "string" } }, - "reviewsTotal": { - "type": "number", - "minimum": 0 + "resultType": { + "description": "Result type requested by the client: 0 for JSON list, 1 for Excel export.", + "example": 0, + "default": 0, + "allOf": [ + { + "$ref": "#/components/schemas/ListResultType" + } + ] + }, + "fileData": { + "type": "string", + "description": "Base64 encoded export file. Present only when resultType is 1.", + "example": "UEsDBBQAAAAIA..." + }, + "nextCursor": { + "type": "string", + "description": "Deprecated for chat message history — use `olderCursor` / `newerCursor` instead. Kept for other cursor-paginated endpoints.", + "format": "uuid", + "nullable": true + }, + "olderCursor": { + "type": "string", + "description": "Message id to pass as `before` to load older messages. Same meaning in every list-messages mode; null when there is no older page.", + "format": "uuid", + "nullable": true + }, + "newerCursor": { + "type": "string", + "description": "Message id to pass as `after` to load newer messages. Same meaning in every list-messages mode; null when there is no newer page.", + "format": "uuid", + "nullable": true + }, + "aroundMessageId": { + "type": "string", + "description": "Present when `around` was requested — the message id the window was centered on.", + "format": "uuid" + }, + "hasMore": { + "type": "boolean", + "description": "Whether another cursor page is available (legacy; prefer hasMoreBefore/hasMoreAfter for chat)." + }, + "hasMoreBefore": { + "type": "boolean", + "description": "True when older messages exist beyond this page (pass `olderCursor` as `before`)." + }, + "hasMoreAfter": { + "type": "boolean", + "description": "True when newer messages exist beyond this page (pass `newerCursor` as `after`)." } }, - "required": ["organizer", "reviews", "reviewsTotal"] + "required": ["page", "pageSize", "totalItemsCount", "totalPages", "sort", "filters"] }, "OrganizerViewerStateResponseDto": { "type": "object", @@ -19044,6 +19186,185 @@ "followingCount" ] }, + "ProfileSummaryTicketDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "subject": { + "type": "string", + "example": "مشکل در پرداخت" + }, + "status": { + "type": "string", + "example": "open" + }, + "category": { + "type": "string", + "example": "payment" + }, + "lastMessageAt": { + "type": "string", + "format": "date-time", + "nullable": true + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["id", "subject", "status", "category", "lastMessageAt", "updatedAt"] + }, + "ProfileSummaryEventPreviewDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string", + "example": "ورکشاپ عکاسی" + }, + "slug": { + "type": "string", + "example": "workshop-akkasi", + "nullable": true + }, + "posterUrl": { + "type": "string", + "format": "uri", + "nullable": true + }, + "squarePosterUrl": { + "type": "string", + "format": "uri", + "nullable": true + }, + "startsAt": { + "type": "string", + "format": "date-time" + } + }, + "required": ["id", "title", "slug", "posterUrl", "squarePosterUrl", "startsAt"] + }, + "ProfileSummaryRegisteredPreviewDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "title": { + "type": "string", + "example": "ورکشاپ عکاسی" + }, + "slug": { + "type": "string", + "example": "workshop-akkasi", + "nullable": true + }, + "posterUrl": { + "type": "string", + "format": "uri", + "nullable": true + }, + "squarePosterUrl": { + "type": "string", + "format": "uri", + "nullable": true + }, + "startsAt": { + "type": "string", + "format": "date-time" + }, + "bookingId": { + "type": "string", + "format": "uuid", + "description": "Booking id" + } + }, + "required": ["id", "title", "slug", "posterUrl", "squarePosterUrl", "startsAt", "bookingId"] + }, + "UserProfileSummaryResponseDto": { + "type": "object", + "properties": { + "walletBalance": { + "type": "number", + "example": 150000, + "minimum": 0 + }, + "followersCount": { + "type": "number", + "example": 12, + "minimum": 0 + }, + "followingCount": { + "type": "number", + "example": 8, + "minimum": 0 + }, + "hostedEventsCount": { + "type": "number", + "example": 3, + "minimum": 0 + }, + "registeredEventsCount": { + "type": "number", + "example": 5, + "minimum": 0 + }, + "bookmarksCount": { + "type": "number", + "example": 7, + "minimum": 0 + }, + "unreadNotificationsCount": { + "type": "number", + "example": 2, + "minimum": 0 + }, + "recentTickets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProfileSummaryTicketDto" + } + }, + "recentHostedEvents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProfileSummaryEventPreviewDto" + } + }, + "recentRegisteredEvents": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProfileSummaryRegisteredPreviewDto" + } + }, + "recentBookmarks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProfileSummaryEventPreviewDto" + } + } + }, + "required": [ + "walletBalance", + "followersCount", + "followingCount", + "hostedEventsCount", + "registeredEventsCount", + "bookmarksCount", + "unreadNotificationsCount", + "recentTickets", + "recentHostedEvents", + "recentRegisteredEvents", + "recentBookmarks" + ] + }, "UpdateUserProfileDto": { "type": "object", "properties": { @@ -19140,108 +19461,161 @@ }, "required": ["firstName", "lastName", "gender", "dateOfBirth", "cityId"] }, - "ListResultType": { - "type": "number", - "enum": [0, 1], - "description": "Result type requested by the client: 0 for JSON list, 1 for Excel export." - }, - "PaginationMetaDto": { + "AdminUserListItemDto": { "type": "object", "properties": { - "page": { - "type": "number", - "description": "Current page number using a 1-based index.", - "example": 1, - "minimum": 1 - }, - "pageSize": { - "type": "number", - "description": "Number of items requested per page.", - "example": 20, - "minimum": 1, - "maximum": 100 - }, - "totalItemsCount": { - "type": "number", - "description": "Total number of items matching the current query.", - "example": 42, - "minimum": 0 - }, - "totalPages": { - "type": "number", - "description": "Total number of pages available for the current query.", - "example": 3, - "minimum": 0 - }, - "sort": { + "id": { "type": "string", - "description": "Applied sort expression. Prefix the field with - for descending order.", - "example": "-createdAt", + "description": "User account identifier.", + "example": "6f6d7d3a-90f2-4ad7-8d52-994a9676c5e1", + "format": "uuid" + }, + "mobile": { + "type": "string", + "description": "Verified Iranian mobile number used as the login identifier.", + "example": "989123456789", + "format": "phone", + "pattern": "^98\\d{10}$" + }, + "firstName": { + "type": "string", + "description": "User given name.", + "example": "Ali", + "nullable": true, "minLength": 1, "maxLength": 100 }, - "filters": { - "type": "object", - "description": "Whitelisted filters applied to the list endpoint.", - "example": { - "status": "pending" - }, - "additionalProperties": { - "type": "string" - } + "lastName": { + "type": "string", + "description": "User family name.", + "example": "SaZa", + "nullable": true, + "minLength": 1, + "maxLength": 100 }, - "resultType": { - "description": "Result type requested by the client: 0 for JSON list, 1 for Excel export.", - "example": 0, - "default": 0, + "gender": { + "description": "User gender.", + "example": "male", + "nullable": true, "allOf": [ { - "$ref": "#/components/schemas/ListResultType" + "$ref": "#/components/schemas/Gender" } ] }, - "fileData": { - "type": "string", - "description": "Base64 encoded export file. Present only when resultType is 1.", - "example": "UEsDBBQAAAAIA..." - }, - "nextCursor": { - "type": "string", - "description": "Deprecated for chat message history — use `olderCursor` / `newerCursor` instead. Kept for other cursor-paginated endpoints.", - "format": "uuid", + "cityId": { + "type": "number", + "description": "Home city identifier.", + "example": 1, + "minimum": 1, "nullable": true }, - "olderCursor": { + "cityName": { "type": "string", - "description": "Message id to pass as `before` to load older messages. Same meaning in every list-messages mode; null when there is no older page.", - "format": "uuid", + "description": "Home city display name.", + "example": "مشهد", "nullable": true }, - "newerCursor": { + "dateOfBirth": { "type": "string", - "description": "Message id to pass as `after` to load newer messages. Same meaning in every list-messages mode; null when there is no newer page.", - "format": "uuid", + "description": "Date of birth as an ISO calendar date (YYYY-MM-DD). Null for profiles completed before the field was required.", + "example": "1991-03-21", + "format": "date", "nullable": true }, - "aroundMessageId": { + "avatarUrl": { "type": "string", - "description": "Present when `around` was requested — the message id the window was centered on.", - "format": "uuid" + "description": "Profile image URL.", + "example": "https://storage.ghabilee.com/users/avatar-123.jpg", + "format": "uri", + "nullable": true }, - "hasMore": { - "type": "boolean", - "description": "Whether another cursor page is available (legacy; prefer hasMoreBefore/hasMoreAfter for chat)." + "bio": { + "type": "string", + "description": "Short profile biography.", + "example": "Event lover and workshop organizer in Tehran.", + "maxLength": 2000, + "nullable": true }, - "hasMoreBefore": { - "type": "boolean", - "description": "True when older messages exist beyond this page (pass `olderCursor` as `before`)." + "defaultAddress": { + "type": "string", + "description": "Fixed physical address for verified hosts.", + "example": "تهران، خیابان ولیعصر، پلاک ۱", + "maxLength": 500, + "nullable": true }, - "hasMoreAfter": { - "type": "boolean", - "description": "True when newer messages exist beyond this page (pass `newerCursor` as `after`)." + "status": { + "description": "Account status.", + "example": "active", + "allOf": [ + { + "$ref": "#/components/schemas/UserStatus" + } + ] + }, + "identityStatus": { + "description": "Host identity verification status; verified users may create events.", + "example": "verified", + "allOf": [ + { + "$ref": "#/components/schemas/IdentityVerificationStatus" + } + ] + }, + "role": { + "description": "User role. Admin is staff-only and manually assigned.", + "example": "user", + "allOf": [ + { + "$ref": "#/components/schemas/UserRole" + } + ] + }, + "followersCount": { + "type": "number", + "description": "Number of users following this account.", + "example": 27, + "minimum": 0 + }, + "followingCount": { + "type": "number", + "description": "Number of organizers this account follows.", + "example": 12, + "minimum": 0 + }, + "userType": { + "type": "string", + "description": "Derived admin-facing user type: admin staff, verified host, or guest.", + "enum": ["guest", "host", "admin"], + "example": "host" + }, + "walletBalance": { + "type": "number", + "description": "Current wallet balance in Tomans.", + "example": 150000, + "minimum": 0 } }, - "required": ["page", "pageSize", "totalItemsCount", "totalPages", "sort", "filters"] + "required": [ + "id", + "mobile", + "firstName", + "lastName", + "gender", + "cityId", + "cityName", + "dateOfBirth", + "avatarUrl", + "bio", + "defaultAddress", + "status", + "identityStatus", + "role", + "followersCount", + "followingCount", + "userType", + "walletBalance" + ] }, "AdminChangeUserMobileDto": { "type": "object", @@ -21111,6 +21485,12 @@ "eventCancellationFeePercent": { "type": "number" }, + "eventCancellationFeePercent12To24Hours": { + "type": "number" + }, + "eventCancellationFeePercentMoreThan24Hours": { + "type": "number" + }, "eventEffectiveCommissionPercent": { "type": "number" }, @@ -21138,6 +21518,8 @@ "eventAddress", "eventPrice", "eventCancellationFeePercent", + "eventCancellationFeePercent12To24Hours", + "eventCancellationFeePercentMoreThan24Hours", "eventEffectiveCommissionPercent", "payableAmount", "eventCategoryId", @@ -21322,7 +21704,21 @@ }, "cancellationFeePercent": { "type": "number", - "example": 10 + "example": 30, + "default": 30, + "description": "Last-minute cancellation fee when 0 < remaining time <= 12 hours." + }, + "cancellationFeePercent12To24Hours": { + "type": "number", + "example": 20, + "default": 20, + "description": "Cancellation fee when 12 < remaining time <= 24 hours." + }, + "cancellationFeePercentMoreThan24Hours": { + "type": "number", + "example": 10, + "default": 10, + "description": "Early cancellation fee when remaining time > 24 hours." }, "settings": { "$ref": "#/components/schemas/EventSettingsDto" @@ -21466,7 +21862,21 @@ }, "cancellationFeePercent": { "type": "number", - "example": 10 + "example": 30, + "default": 30, + "description": "Last-minute cancellation fee when 0 < remaining time <= 12 hours." + }, + "cancellationFeePercent12To24Hours": { + "type": "number", + "example": 20, + "default": 20, + "description": "Cancellation fee when 12 < remaining time <= 24 hours." + }, + "cancellationFeePercentMoreThan24Hours": { + "type": "number", + "example": 10, + "default": 10, + "description": "Early cancellation fee when remaining time > 24 hours." }, "settings": { "$ref": "#/components/schemas/EventSettingsDto" @@ -21638,7 +22048,16 @@ ] }, "cancellationFeePercent": { - "type": "number" + "type": "number", + "description": "Last-minute fee for 0 < remaining time <= 12 hours." + }, + "cancellationFeePercent12To24Hours": { + "type": "number", + "description": "Fee for 12 < remaining time <= 24 hours." + }, + "cancellationFeePercentMoreThan24Hours": { + "type": "number", + "description": "Early fee for remaining time > 24 hours." }, "commissionPercent": { "type": "number", @@ -21738,6 +22157,8 @@ "genderRestriction", "ageRestriction", "cancellationFeePercent", + "cancellationFeePercent12To24Hours", + "cancellationFeePercentMoreThan24Hours", "commissionPercent", "effectiveCommissionPercent", "status", @@ -22039,9 +22460,6 @@ } ] }, - "mobile": { - "type": "string" - }, "firstName": { "type": "object", "nullable": true @@ -22070,7 +22488,7 @@ "type": "string" } }, - "required": ["bookingId", "userId", "bookingCode", "status", "mobile", "createdAt"] + "required": ["bookingId", "userId", "bookingCode", "status", "createdAt"] }, "EventAttendeeFilter": { "type": "string", @@ -22168,7 +22586,21 @@ }, "cancellationFeePercent": { "type": "number", - "example": 10 + "example": 30, + "default": 30, + "description": "Last-minute cancellation fee when 0 < remaining time <= 12 hours." + }, + "cancellationFeePercent12To24Hours": { + "type": "number", + "example": 20, + "default": 20, + "description": "Cancellation fee when 12 < remaining time <= 24 hours." + }, + "cancellationFeePercentMoreThan24Hours": { + "type": "number", + "example": 10, + "default": 10, + "description": "Early cancellation fee when remaining time > 24 hours." }, "settings": { "$ref": "#/components/schemas/EventSettingsDto" @@ -22326,7 +22758,16 @@ ] }, "cancellationFeePercent": { - "type": "number" + "type": "number", + "description": "Last-minute fee for 0 < remaining time <= 12 hours." + }, + "cancellationFeePercent12To24Hours": { + "type": "number", + "description": "Fee for 12 < remaining time <= 24 hours." + }, + "cancellationFeePercentMoreThan24Hours": { + "type": "number", + "description": "Early fee for remaining time > 24 hours." }, "commissionPercent": { "type": "number", @@ -22429,6 +22870,8 @@ "genderRestriction", "ageRestriction", "cancellationFeePercent", + "cancellationFeePercent12To24Hours", + "cancellationFeePercentMoreThan24Hours", "commissionPercent", "effectiveCommissionPercent", "status", @@ -22557,6 +23000,41 @@ "contactLinks" ] }, + "ReviewStatus": { + "type": "string", + "enum": ["published", "hidden", "deleted"], + "description": "Publication status." + }, + "ReviewUserSummaryDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "User identifier.", + "example": "c3cc67fb-8615-432c-9a57-a5249c4a6b1c", + "format": "uuid" + }, + "firstName": { + "type": "string", + "description": "User first name.", + "example": "Ali", + "nullable": true + }, + "lastName": { + "type": "string", + "description": "User last name.", + "example": "Rezaei", + "nullable": true + }, + "avatarUrl": { + "type": "string", + "description": "Public profile avatar URL when the user has uploaded one.", + "example": "https://cdn.example.com/avatars/u1.jpg", + "nullable": true + } + }, + "required": ["id", "firstName", "lastName", "avatarUrl"] + }, "ReviewResponseDto": { "type": "object", "properties": { @@ -22975,7 +23453,16 @@ ] }, "cancellationFeePercent": { - "type": "number" + "type": "number", + "description": "Last-minute fee for 0 < remaining time <= 12 hours." + }, + "cancellationFeePercent12To24Hours": { + "type": "number", + "description": "Fee for 12 < remaining time <= 24 hours." + }, + "cancellationFeePercentMoreThan24Hours": { + "type": "number", + "description": "Early fee for remaining time > 24 hours." }, "commissionPercent": { "type": "number", @@ -23083,6 +23570,8 @@ "genderRestriction", "ageRestriction", "cancellationFeePercent", + "cancellationFeePercent12To24Hours", + "cancellationFeePercentMoreThan24Hours", "commissionPercent", "effectiveCommissionPercent", "status", @@ -23398,11 +23887,6 @@ "FollowUserSummaryDto": { "type": "object", "properties": { - "mobile": { - "type": "string", - "description": "User mobile number. Omitted on the public organizers/:id/followers list.", - "example": "989123456789" - }, "firstName": { "type": "string", "description": "User first name.", @@ -23613,7 +24097,8 @@ "format": "uuid" }, "mobile": { - "type": "string" + "type": "string", + "description": "Present for host-entered manual contacts. Omitted for previous_attendees (users.mobile must not leak to hosts)." }, "firstName": { "type": "string", @@ -23637,7 +24122,7 @@ "type": "string" } }, - "required": ["id", "listId", "mobile", "firstName", "lastName", "userId", "note", "createdAt"] + "required": ["id", "listId", "firstName", "lastName", "userId", "note", "createdAt"] }, "AddGuestListItemDto": { "type": "object", @@ -23687,7 +24172,8 @@ "nullable": true }, "mobile": { - "type": "string" + "type": "string", + "description": "Admin-only. Host endpoints omit mobile so users.mobile never leaks to organizers." }, "firstName": { "type": "string", @@ -23698,7 +24184,7 @@ "nullable": true } }, - "required": ["userId", "mobile", "firstName", "lastName"] + "required": ["userId", "firstName", "lastName"] }, "GuestListNotifyResultDto": { "type": "object", @@ -24358,7 +24844,7 @@ }, "WalletTransactionType": { "type": "string", - "enum": ["booking_payment", "refund_guest_cancel", "refund_event_cancel", "withdrawal", "deposit"] + "enum": ["booking_payment", "refund_guest_cancel", "refund_event_cancel", "organizer_earning", "withdrawal", "deposit"] }, "WalletTransactionResponseDto": { "type": "object", @@ -25388,10 +25874,13 @@ "type": "string", "format": "uuid" }, - "userMobile": { + "userFirstName": { "type": "string", - "nullable": true, - "example": "0912***4567" + "nullable": true + }, + "userLastName": { + "type": "string", + "nullable": true }, "listAmount": { "type": "number", @@ -25583,6 +26072,12 @@ "eventCancellationFeePercent": { "type": "number" }, + "eventCancellationFeePercent12To24Hours": { + "type": "number" + }, + "eventCancellationFeePercentMoreThan24Hours": { + "type": "number" + }, "eventEffectiveCommissionPercent": { "type": "number" }, @@ -25618,6 +26113,8 @@ "eventIsFree", "eventPrice", "eventCancellationFeePercent", + "eventCancellationFeePercent12To24Hours", + "eventCancellationFeePercentMoreThan24Hours", "eventEffectiveCommissionPercent", "discountCode", "discountAmount", @@ -25916,6 +26413,12 @@ "eventCancellationFeePercent": { "type": "number" }, + "eventCancellationFeePercent12To24Hours": { + "type": "number" + }, + "eventCancellationFeePercentMoreThan24Hours": { + "type": "number" + }, "eventEffectiveCommissionPercent": { "type": "number" }, @@ -25957,6 +26460,8 @@ "eventIsFree", "eventPrice", "eventCancellationFeePercent", + "eventCancellationFeePercent12To24Hours", + "eventCancellationFeePercentMoreThan24Hours", "eventEffectiveCommissionPercent", "discountCode", "discountAmount", @@ -25966,6 +26471,112 @@ "user" ] }, + "OrganizerReviewResponseDto": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Review identifier.", + "example": "16e33f70-d3cc-492c-a8fa-e2317fbfb37b", + "format": "uuid" + }, + "eventId": { + "type": "string", + "description": "Reviewed event identifier.", + "example": "6f6d7d3a-90f2-4ad7-8d52-994a9676c5e1", + "format": "uuid" + }, + "userId": { + "type": "string", + "description": "Guest who wrote the review.", + "example": "c3cc67fb-8615-432c-9a57-a5249c4a6b1c", + "format": "uuid" + }, + "bookingId": { + "type": "string", + "description": "Booking that earned the right to review.", + "example": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", + "format": "uuid" + }, + "rating": { + "type": "number", + "description": "Star rating from 1 to 5.", + "example": 5, + "minimum": 1, + "maximum": 5 + }, + "body": { + "type": "string", + "description": "Optional review text; null when the guest rated without a comment.", + "example": "Great event — well organized and welcoming.", + "nullable": true + }, + "hostReplyBody": { + "type": "string", + "description": "Organizer reply text, when present.", + "example": "Thank you for attending!", + "nullable": true + }, + "hostReplyAt": { + "type": "string", + "description": "Timestamp when the organizer first replied.", + "example": "2026-08-15T09:00:00.000Z", + "format": "date-time", + "nullable": true + }, + "status": { + "description": "Publication status.", + "example": "published", + "allOf": [ + { + "$ref": "#/components/schemas/ReviewStatus" + } + ] + }, + "createdAt": { + "type": "string", + "description": "Review creation timestamp.", + "example": "2026-08-14T09:00:00.000Z", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "description": "Last update timestamp.", + "example": "2026-08-15T09:00:00.000Z", + "format": "date-time" + }, + "user": { + "description": "Guest who wrote the review (public name and optional avatar).", + "allOf": [ + { + "$ref": "#/components/schemas/ReviewUserSummaryDto" + } + ] + }, + "eventTitle": { + "type": "string", + "description": "Event title for organizer-wide review lists." + }, + "eventSlug": { + "type": "string", + "description": "Event slug for linking to the public event page." + } + }, + "required": [ + "id", + "eventId", + "userId", + "bookingId", + "rating", + "body", + "status", + "createdAt", + "updatedAt", + "user", + "eventTitle", + "eventSlug" + ] + }, "CreateReviewDto": { "type": "object", "properties": { @@ -26743,11 +27354,6 @@ "UserBlockUserSummaryDto": { "type": "object", "properties": { - "mobile": { - "type": "string", - "description": "Blocked user mobile number.", - "example": "989123456789" - }, "firstName": { "type": "string", "description": "Blocked user first name.", @@ -26761,7 +27367,7 @@ "nullable": true } }, - "required": ["mobile", "firstName", "lastName"] + "required": ["firstName", "lastName"] }, "UserBlockResponseDto": { "type": "object",