207 lines
5.8 KiB
TypeScript
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})`,
|
|
);
|
|
}
|
|
}
|
|
}
|