Update carousels, card hover, and IranBay branding.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { MeshkeeClient } from '../meshkee/meshkee.client';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import {
|
||||
CreateBusinessDto,
|
||||
ListBusinessesDto,
|
||||
RegisterShopDto,
|
||||
UpdateBusinessDto,
|
||||
} from './dto/business.dto';
|
||||
|
||||
function slugify(value: string) {
|
||||
return value
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, '-')
|
||||
.replace(/^-+|-+$/g, '')
|
||||
.slice(0, 80);
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class BusinessesService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly meshkee: MeshkeeClient,
|
||||
) {}
|
||||
|
||||
async list(query: ListBusinessesDto) {
|
||||
const page = query.page ?? 1;
|
||||
const limit = Math.min(query.limit ?? 20, 100);
|
||||
const skip = (page - 1) * limit;
|
||||
|
||||
const where: Prisma.BusinessWhereInput = {};
|
||||
|
||||
if (query.status && query.status !== 'all') {
|
||||
where.status = query.status;
|
||||
} else if (!query.status) {
|
||||
where.status = 'published';
|
||||
}
|
||||
|
||||
if (query.featured === true) {
|
||||
where.isFeatured = true;
|
||||
}
|
||||
|
||||
if (query.q) {
|
||||
where.slug = { contains: query.q, mode: 'insensitive' };
|
||||
}
|
||||
|
||||
const [items, total] = await Promise.all([
|
||||
this.prisma.business.findMany({
|
||||
where,
|
||||
orderBy: [
|
||||
{ isFeatured: 'desc' },
|
||||
{ sortWeight: 'desc' },
|
||||
{ publishedAt: 'desc' },
|
||||
{ id: 'desc' },
|
||||
],
|
||||
skip,
|
||||
take: limit,
|
||||
}),
|
||||
this.prisma.business.count({ where }),
|
||||
]);
|
||||
|
||||
return { items, page, limit, total };
|
||||
}
|
||||
|
||||
async getBySlug(slug: string) {
|
||||
const business = await this.prisma.business.findUnique({ where: { slug } });
|
||||
if (!business) {
|
||||
throw new NotFoundException(`Business "${slug}" not found`);
|
||||
}
|
||||
return business;
|
||||
}
|
||||
|
||||
async getById(id: string) {
|
||||
const business = await this.prisma.business.findUnique({
|
||||
where: { id: BigInt(id) },
|
||||
});
|
||||
if (!business) {
|
||||
throw new NotFoundException(`Business ${id} not found`);
|
||||
}
|
||||
return business;
|
||||
}
|
||||
|
||||
/**
|
||||
* Self-serve shop signup:
|
||||
* 1) Meshkee business owned by the logged-in IranBay user (NOT the iranbay.com tenant)
|
||||
* 2) IranBay marketplace listing (pending_review)
|
||||
*/
|
||||
async registerShop(dto: RegisterShopDto, authorization?: string) {
|
||||
if (!authorization?.trim()) {
|
||||
throw new UnauthorizedException('Sign in required');
|
||||
}
|
||||
|
||||
const user = await this.meshkee.getMe(authorization);
|
||||
const name = dto.name.trim();
|
||||
if (name.length < 2) {
|
||||
throw new BadRequestException('Business name is required');
|
||||
}
|
||||
|
||||
const slug = (dto.slug?.trim() || slugify(name)).slice(0, 100);
|
||||
if (!slug) {
|
||||
throw new BadRequestException('Could not build a slug from the name');
|
||||
}
|
||||
|
||||
let categoryIds = dto.meshkeeCategoryIds?.length
|
||||
? this.parseMeshkeeCategoryIds(dto.meshkeeCategoryIds)
|
||||
: [];
|
||||
|
||||
if (!categoryIds.length) {
|
||||
const categories = await this.meshkee.listBusinessCategories();
|
||||
const other =
|
||||
categories.find((item) => String(item.slug) === 'other') ??
|
||||
categories[0];
|
||||
if (!other) {
|
||||
throw new BadRequestException('No Meshkee categories available');
|
||||
}
|
||||
categoryIds = [Number(other.id)];
|
||||
}
|
||||
|
||||
const firstName =
|
||||
user.firstNameEn?.trim() || user.firstName?.trim() || 'Owner';
|
||||
const lastName =
|
||||
user.lastNameEn?.trim() || user.lastName?.trim() || firstName;
|
||||
|
||||
const created = await this.meshkee.createBusiness({
|
||||
name,
|
||||
nameFa: name,
|
||||
slug,
|
||||
about: dto.about?.trim() || undefined,
|
||||
categoryIds,
|
||||
ownerFirstName: firstName,
|
||||
ownerLastName: lastName.length >= 2 ? lastName : firstName,
|
||||
ownerFirstNameEn: firstName,
|
||||
ownerLastNameEn: lastName.length >= 2 ? lastName : firstName,
|
||||
ownerCellNumber: user.cellNumber,
|
||||
ownerPassword: dto.ownerPassword,
|
||||
});
|
||||
|
||||
try {
|
||||
const listing = await this.prisma.business.create({
|
||||
data: {
|
||||
meshkeeBusinessId: BigInt(created.id),
|
||||
slug: created.slug || slug,
|
||||
status: 'pending_review',
|
||||
isFeatured: false,
|
||||
sortWeight: 0,
|
||||
publishedAt: null,
|
||||
},
|
||||
});
|
||||
return {
|
||||
listing,
|
||||
meshkeeBusinessId: created.id,
|
||||
message:
|
||||
'Shop created in Meshkee and submitted to IranBay for review.',
|
||||
};
|
||||
} catch (error) {
|
||||
this.rethrowUnique(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async create(dto: CreateBusinessDto, authorization?: string) {
|
||||
const status = dto.status ?? 'draft';
|
||||
const now = new Date();
|
||||
|
||||
const meshkeePatch: {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
categoryIds?: number[];
|
||||
} = { slug: dto.slug };
|
||||
|
||||
if (dto.name?.trim()) meshkeePatch.name = dto.name.trim();
|
||||
if (dto.meshkeeCategoryIds?.length) {
|
||||
meshkeePatch.categoryIds = this.parseMeshkeeCategoryIds(
|
||||
dto.meshkeeCategoryIds,
|
||||
);
|
||||
}
|
||||
|
||||
await this.meshkee.updateBusiness(
|
||||
dto.meshkeeBusinessId,
|
||||
meshkeePatch,
|
||||
authorization,
|
||||
);
|
||||
|
||||
try {
|
||||
return await this.prisma.business.create({
|
||||
data: {
|
||||
meshkeeBusinessId: BigInt(dto.meshkeeBusinessId),
|
||||
slug: dto.slug,
|
||||
status,
|
||||
isFeatured: dto.isFeatured ?? false,
|
||||
sortWeight: dto.sortWeight ?? 0,
|
||||
publishedAt: status === 'published' ? now : null,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
this.rethrowUnique(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateBusinessDto, authorization?: string) {
|
||||
const existing = await this.getById(id);
|
||||
|
||||
const data: Prisma.BusinessUpdateInput = {
|
||||
slug: dto.slug,
|
||||
status: dto.status,
|
||||
isFeatured: dto.isFeatured,
|
||||
sortWeight: dto.sortWeight,
|
||||
};
|
||||
|
||||
if (dto.status === 'published' && existing.status !== 'published') {
|
||||
data.publishedAt = new Date();
|
||||
}
|
||||
|
||||
const meshkeePatch: {
|
||||
name?: string;
|
||||
slug?: string;
|
||||
categoryIds?: number[];
|
||||
} = {};
|
||||
if (dto.name !== undefined) meshkeePatch.name = dto.name;
|
||||
if (dto.slug !== undefined) meshkeePatch.slug = dto.slug;
|
||||
if (dto.meshkeeCategoryIds !== undefined) {
|
||||
meshkeePatch.categoryIds = this.parseMeshkeeCategoryIds(
|
||||
dto.meshkeeCategoryIds,
|
||||
);
|
||||
}
|
||||
|
||||
if (Object.keys(meshkeePatch).length) {
|
||||
await this.meshkee.updateBusiness(
|
||||
String(existing.meshkeeBusinessId),
|
||||
meshkeePatch,
|
||||
authorization,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
return await this.prisma.business.update({
|
||||
where: { id: BigInt(id) },
|
||||
data,
|
||||
});
|
||||
} catch (error) {
|
||||
this.rethrowUnique(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string) {
|
||||
await this.getById(id);
|
||||
await this.prisma.business.delete({ where: { id: BigInt(id) } });
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
private parseMeshkeeCategoryIds(ids: string[]): number[] {
|
||||
const parsed = [...new Set(ids)].map((id) => {
|
||||
const n = Number(id);
|
||||
if (!Number.isInteger(n) || n <= 0) {
|
||||
throw new BadRequestException(`Invalid Meshkee category id: ${id}`);
|
||||
}
|
||||
return n;
|
||||
});
|
||||
if (!parsed.length) {
|
||||
throw new BadRequestException('meshkeeCategoryIds cannot be empty');
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private rethrowUnique(error: unknown): void {
|
||||
if (
|
||||
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
error.code === 'P2002'
|
||||
) {
|
||||
const target = Array.isArray(error.meta?.target)
|
||||
? error.meta.target.join(', ')
|
||||
: 'field';
|
||||
throw new BadRequestException(`Business ${target} already exists`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user