Files
backend/src/meshkee/meshkee.client.ts
T
2026-10-03 23:17:33 +03:30

207 lines
5.8 KiB
TypeScript

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<string>('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<string>('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<MeshkeeAuthUser> {
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<void> {
const auth = this.authHeader(authorization);
if (!auth) {
this.logger.warn(
`Skipping Meshkee update for business ${meshkeeBusinessId}: no auth token`,
);
return;
}
const payload: Record<string, unknown> = {};
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})`,
);
}
}
}