import { BadGatewayException, Injectable, Logger, UnauthorizedException, } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; export type MeshkeeBusinessUpdate = { name?: string; slug?: string; nameFa?: string; about?: string; categoryIds?: number[]; }; export type MeshkeeCreateBusinessInput = { name: string; nameFa: string; slug?: string; about?: string; categoryIds: number[]; ownerFirstName: string; ownerLastName: string; ownerFirstNameEn: string; ownerLastNameEn: string; ownerCellNumber: string; ownerPassword: string; }; export type MeshkeeAuthUser = { id: string; cellNumber: string; email: string | null; firstName: string | null; lastName: string | null; firstNameEn: string | null; lastNameEn: string | null; dashboard: string; }; @Injectable() export class MeshkeeClient { private readonly logger = new Logger(MeshkeeClient.name); constructor(private readonly config: ConfigService) {} private apiBase(): string { return ( this.config.get('MESHKEE_API_URL')?.replace(/\/$/, '') || 'http://localhost:3010/api/v1' ); } private authHeader(authorization?: string): string | null { if (authorization?.trim()) return authorization.trim(); const token = this.config.get('MESHKEE_API_TOKEN')?.trim(); return token ? `Bearer ${token}` : null; } private serviceAuth(): string { const auth = this.authHeader(); if (!auth) { throw new UnauthorizedException( 'MESHKEE_API_TOKEN is required for Meshkee admin operations', ); } return auth; } private async parseError(response: Response, fallback: string) { const text = await response.text().catch(() => ''); try { const json = JSON.parse(text) as { message?: string | string[] }; if (Array.isArray(json.message)) return json.message.join(', '); if (typeof json.message === 'string') return json.message; } catch { /* ignore */ } return text.slice(0, 300) || fallback; } async getMe(authorization: string): Promise { const response = await fetch(`${this.apiBase()}/auth/me`, { headers: { Authorization: authorization }, }); if (!response.ok) { throw new UnauthorizedException('Invalid or expired Meshkee session'); } const payload = (await response.json()) as { user: MeshkeeAuthUser }; return payload.user; } async listBusinessCategories(authorization?: string) { const auth = this.authHeader(authorization) ?? this.serviceAuth(); const response = await fetch(`${this.apiBase()}/business-categories`, { headers: { Authorization: auth }, }); if (!response.ok) { throw new BadGatewayException( await this.parseError(response, 'Failed to list Meshkee categories'), ); } const payload = (await response.json()) as { items?: Array<{ id: string | number; slug?: string }>; }; return payload.items ?? []; } async createBusiness( body: MeshkeeCreateBusinessInput, authorization?: string, ) { const auth = this.authHeader(authorization) ?? this.serviceAuth(); const response = await fetch(`${this.apiBase()}/businesses`, { method: 'POST', headers: { Authorization: auth, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }); if (!response.ok) { throw new BadGatewayException( await this.parseError(response, 'Failed to create Meshkee business'), ); } const data = (await response.json()) as { id: string | number; slug?: string; name?: string; }; return { ...data, id: String(data.id) }; } async addDomain(businessId: string, host: string, authorization?: string) { const auth = this.authHeader(authorization) ?? this.serviceAuth(); const response = await fetch( `${this.apiBase()}/businesses/${businessId}/domains`, { method: 'POST', headers: { Authorization: auth, 'Content-Type': 'application/json', }, body: JSON.stringify({ host, isPrimary: true, updateDns: false, }), }, ); if (!response.ok) { throw new BadGatewayException( await this.parseError(response, 'Failed to attach Meshkee domain'), ); } return response.json(); } /** Write profile / category fields to Meshkee (source of truth for content). */ async updateBusiness( meshkeeBusinessId: string, body: MeshkeeBusinessUpdate, authorization?: string, ): Promise { const auth = this.authHeader(authorization); if (!auth) { this.logger.warn( `Skipping Meshkee update for business ${meshkeeBusinessId}: no auth token`, ); return; } const payload: Record = {}; if (body.name !== undefined) payload.name = body.name; if (body.slug !== undefined) payload.slug = body.slug; if (body.nameFa !== undefined) payload.nameFa = body.nameFa; if (body.about !== undefined) payload.about = body.about; if (body.categoryIds !== undefined) payload.categoryIds = body.categoryIds; if (Object.keys(payload).length === 0) return; const response = await fetch( `${this.apiBase()}/businesses/${meshkeeBusinessId}`, { method: 'PATCH', headers: { Authorization: auth, 'Content-Type': 'application/json', }, body: JSON.stringify(payload), }, ); if (!response.ok) { const text = await response.text().catch(() => ''); this.logger.error( `Meshkee update failed (${response.status}): ${text.slice(0, 300)}`, ); throw new BadGatewayException( `Failed to update Meshkee business (${response.status})`, ); } } }