Files
backend/src/businesses/businesses.service.ts
T

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`);
}
}
}