mirror of
https://git.meshkee.com/Meshkee/backend.git
synced 2026-08-12 06:40:58 +04:30
Add public user-products storefront API and website docs.
Expose published customer listings under /tenants/:host/user-products (list, search, details, technical-info) and document them in the website API pack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
158523df7b
commit
953b87b616
@@ -0,0 +1,176 @@
|
||||
import { Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsIn,
|
||||
IsInt,
|
||||
IsNumber,
|
||||
IsOptional,
|
||||
IsString,
|
||||
Min,
|
||||
MinLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { ContentStatus, UserProductCondition } from '@prisma/client';
|
||||
|
||||
export const USER_PRODUCT_PRICE_CURRENCIES = ['IRT', 'USD', 'EUR', 'AED'] as const;
|
||||
export type UserProductPriceCurrency =
|
||||
(typeof USER_PRODUCT_PRICE_CURRENCIES)[number];
|
||||
|
||||
export class ListMyUserProductsDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
pageSize?: number;
|
||||
}
|
||||
|
||||
export class ListAdminUserProductsDto extends ListMyUserProductsDto {
|
||||
@IsOptional()
|
||||
@IsEnum(ContentStatus)
|
||||
status?: ContentStatus;
|
||||
}
|
||||
|
||||
export class ListPublicUserProductsDto {
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
page?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
pageSize?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
q?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
categoryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
cityId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
countryId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(UserProductCondition)
|
||||
condition?: UserProductCondition;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => value === 'true' || value === true)
|
||||
@IsBoolean()
|
||||
promoted?: boolean;
|
||||
}
|
||||
|
||||
export class UserProductTechnicalValueDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
fieldId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
textValue?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
optionId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsString({ each: true })
|
||||
optionIds?: string[];
|
||||
}
|
||||
|
||||
export class CreateUserProductDto {
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
titleFa!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
titleEn?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
categoryId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Number)
|
||||
@IsNumber({ maxDecimalPlaces: 2 })
|
||||
@Min(0)
|
||||
price?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn(USER_PRODUCT_PRICE_CURRENCIES)
|
||||
priceCurrency?: UserProductPriceCurrency;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
priceByExpert?: boolean;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
countryId!: string;
|
||||
|
||||
@IsString()
|
||||
@MinLength(1)
|
||||
cityId!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
deliveryNote?: string;
|
||||
|
||||
@IsEnum(UserProductCondition)
|
||||
condition!: UserProductCondition;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
technicalNotes?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => UserProductTechnicalValueDto)
|
||||
technicalValues?: UserProductTechnicalValueDto[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
featuredMediaId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
galleryMediaIds?: string[];
|
||||
}
|
||||
|
||||
export class UpdateUserProductDto extends CreateUserProductDto {}
|
||||
|
||||
export class UpdateUserProductStatusDto {
|
||||
@IsEnum(ContentStatus)
|
||||
status!: ContentStatus;
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { RequireBusinessPermission } from '../auth/decorators/require-business-permission.decorator';
|
||||
import { BusinessPermissionGuard } from '../auth/guards/business-permission.guard';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import {
|
||||
CreateUserProductDto,
|
||||
ListAdminUserProductsDto,
|
||||
UpdateUserProductDto,
|
||||
UpdateUserProductStatusDto,
|
||||
} from './dto/user-product.dto';
|
||||
import { UserProductsService } from './user-products.service';
|
||||
|
||||
@Controller('businesses/:businessId/user-products')
|
||||
@UseGuards(JwtAuthGuard, BusinessPermissionGuard)
|
||||
export class UserProductsAdminController {
|
||||
constructor(private readonly service: UserProductsService) {}
|
||||
|
||||
@Get('categories')
|
||||
@RequireBusinessPermission('user_products.read')
|
||||
listCategories(
|
||||
@Param('businessId') businessId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.listCategoriesForAdmin(businessId, user);
|
||||
}
|
||||
|
||||
@Get('categories/:categoryId/technical-form')
|
||||
@RequireBusinessPermission('user_products.read')
|
||||
getCategoryTechnicalForm(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('categoryId') categoryId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.getCategoryTechnicalFormForAdmin(
|
||||
businessId,
|
||||
categoryId,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@Get()
|
||||
@RequireBusinessPermission('user_products.read')
|
||||
list(
|
||||
@Param('businessId') businessId: string,
|
||||
@Query() query: ListAdminUserProductsDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.adminList(businessId, query, user);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequireBusinessPermission('user_products.create')
|
||||
create(
|
||||
@Param('businessId') businessId: string,
|
||||
@Body() dto: CreateUserProductDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.adminCreate(businessId, dto, user);
|
||||
}
|
||||
|
||||
@Get(':productId')
|
||||
@RequireBusinessPermission('user_products.read')
|
||||
getOne(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('productId') productId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.adminGetOne(businessId, productId, user);
|
||||
}
|
||||
|
||||
@Patch(':productId')
|
||||
@RequireBusinessPermission('user_products.update')
|
||||
update(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('productId') productId: string,
|
||||
@Body() dto: UpdateUserProductDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.adminUpdate(businessId, productId, dto, user);
|
||||
}
|
||||
|
||||
@Patch(':productId/status')
|
||||
@RequireBusinessPermission('user_products.update')
|
||||
updateStatus(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('productId') productId: string,
|
||||
@Body() dto: UpdateUserProductStatusDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.adminUpdateStatus(businessId, productId, dto, user);
|
||||
}
|
||||
|
||||
@Post(':productId/promote')
|
||||
@RequireBusinessPermission('user_products.update')
|
||||
promote(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('productId') productId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.adminPromote(businessId, productId, user);
|
||||
}
|
||||
|
||||
@Delete(':productId')
|
||||
@RequireBusinessPermission('user_products.delete')
|
||||
remove(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('productId') productId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.adminRemove(businessId, productId, user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Query,
|
||||
UploadedFiles,
|
||||
UseGuards,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { FilesInterceptor } from '@nestjs/platform-express';
|
||||
import { memoryStorage } from 'multer';
|
||||
import { AuthUser } from '../auth/auth.types';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
|
||||
import {
|
||||
CreateUserProductDto,
|
||||
ListMyUserProductsDto,
|
||||
UpdateUserProductDto,
|
||||
} from './dto/user-product.dto';
|
||||
import { UserProductsService } from './user-products.service';
|
||||
|
||||
@Controller('businesses/:businessId/my-user-products')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class UserProductsController {
|
||||
constructor(private readonly service: UserProductsService) {}
|
||||
|
||||
@Get('categories')
|
||||
listCategories(
|
||||
@Param('businessId') businessId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.listCategories(businessId, user);
|
||||
}
|
||||
|
||||
@Get('categories/:categoryId/technical-form')
|
||||
getCategoryTechnicalForm(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('categoryId') categoryId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.getCategoryTechnicalForm(
|
||||
businessId,
|
||||
categoryId,
|
||||
user,
|
||||
);
|
||||
}
|
||||
|
||||
@Post('media')
|
||||
@UseInterceptors(
|
||||
FilesInterceptor('files', 10, {
|
||||
storage: memoryStorage(),
|
||||
}),
|
||||
)
|
||||
uploadMedia(
|
||||
@Param('businessId') businessId: string,
|
||||
@UploadedFiles() files: Express.Multer.File[],
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.uploadMedia(businessId, files ?? [], user);
|
||||
}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@Param('businessId') businessId: string,
|
||||
@Query() query: ListMyUserProductsDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.list(businessId, query, user);
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(
|
||||
@Param('businessId') businessId: string,
|
||||
@Body() dto: CreateUserProductDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.create(businessId, dto, user);
|
||||
}
|
||||
|
||||
@Get(':productId')
|
||||
getOne(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('productId') productId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.getOne(businessId, productId, user);
|
||||
}
|
||||
|
||||
@Patch(':productId')
|
||||
update(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('productId') productId: string,
|
||||
@Body() dto: UpdateUserProductDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.update(businessId, productId, dto, user);
|
||||
}
|
||||
|
||||
@Post(':productId/promote')
|
||||
promote(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('productId') productId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.promote(businessId, productId, user);
|
||||
}
|
||||
|
||||
@Delete(':productId')
|
||||
remove(
|
||||
@Param('businessId') businessId: string,
|
||||
@Param('productId') productId: string,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
return this.service.remove(businessId, productId, user);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { CategoriesModule } from '../categories/categories.module';
|
||||
import { MediaModule } from '../media/media.module';
|
||||
import { TenantModule } from '../tenant/tenant.module';
|
||||
import { UserProductsAdminController } from './user-products.admin.controller';
|
||||
import { UserProductsController } from './user-products.controller';
|
||||
import { PublicUserProductsController } from './user-products.public.controller';
|
||||
import { UserProductsService } from './user-products.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, CategoriesModule, MediaModule, TenantModule],
|
||||
controllers: [
|
||||
UserProductsController,
|
||||
UserProductsAdminController,
|
||||
PublicUserProductsController,
|
||||
],
|
||||
providers: [UserProductsService],
|
||||
})
|
||||
export class UserProductsModule {}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Controller, Get, Param, Query } from '@nestjs/common';
|
||||
import { ListPublicUserProductsDto } from './dto/user-product.dto';
|
||||
import { UserProductsService } from './user-products.service';
|
||||
|
||||
@Controller('tenants/:host/user-products')
|
||||
export class PublicUserProductsController {
|
||||
constructor(private readonly service: UserProductsService) {}
|
||||
|
||||
@Get()
|
||||
list(
|
||||
@Param('host') host: string,
|
||||
@Query() query: ListPublicUserProductsDto,
|
||||
) {
|
||||
return this.service.listPublic(host, query);
|
||||
}
|
||||
|
||||
@Get(':slug/technical-info')
|
||||
getTechnicalInfo(@Param('host') host: string, @Param('slug') slug: string) {
|
||||
return this.service.getPublicTechnicalInfoBySlug(host, slug);
|
||||
}
|
||||
|
||||
@Get(':slug')
|
||||
getBySlug(@Param('host') host: string, @Param('slug') slug: string) {
|
||||
return this.service.getPublicBySlug(host, slug);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user