286 lines
7.5 KiB
TypeScript
286 lines
7.5 KiB
TypeScript
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`);
|
|
}
|
|
}
|
|
}
|