P1 complete: real انطلق maps + Nabeh WhatsApp OTP + chat + ratings + cancel(both) + fraud detection
- maps: real map-saas route/geocode/reverse/places (x-api-key per country) + straight-line fallback - auth: Redis-backed OTP + Nabeh WhatsApp send (dev mode keeps fixed 1234 bypass) - chat: per-trip messages (participant-guarded) + socket broadcast - ratings: post-trip rating + driver avg recompute + no double-rate - cancel: from rider or driver + stage-based cancel fee + notify both - fraud: cancel-abuse (soft/hard block via Redis), arrived-far-from-pickup, completed-too-fast → fraud_flags - migration InitP1b (chat_messages, ratings, fraud_flags, trips.cancelled_by/cancel_fee) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { NabehService } from './nabeh.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
providers: [NabehService],
|
||||
exports: [NabehService],
|
||||
})
|
||||
export class NabehModule {}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
|
||||
/**
|
||||
* محوّل Nabeh لإرسال OTP عبر واتساب.
|
||||
* التدفق: login (يُخزَّن التوكن) ثم otp/send برمزنا نحن.
|
||||
*/
|
||||
@Injectable()
|
||||
export class NabehService {
|
||||
private readonly logger = new Logger('Nabeh');
|
||||
private token: string | null = null;
|
||||
private tokenExp = 0;
|
||||
|
||||
constructor(private readonly config: ConfigService) {}
|
||||
|
||||
private get base() {
|
||||
return this.config.get<string>('nabeh.baseUrl');
|
||||
}
|
||||
|
||||
private async ensureToken(): Promise<string> {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
if (this.token && now < this.tokenExp - 60) return this.token;
|
||||
|
||||
const res = await fetch(`${this.base}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: this.config.get<string>('nabeh.email'),
|
||||
password: this.config.get<string>('nabeh.password'),
|
||||
}),
|
||||
});
|
||||
if (!res.ok) throw new Error(`Nabeh login failed: ${res.status}`);
|
||||
const data: any = await res.json();
|
||||
const token = data.token || data.access_token || data.data?.token;
|
||||
if (!token) throw new Error('Nabeh login: no token in response');
|
||||
this.token = token;
|
||||
// exp من JWT إن وُجد، وإلا افتراض ساعة
|
||||
this.tokenExp = NabehService.jwtExp(token) ?? now + 3600;
|
||||
return token;
|
||||
}
|
||||
|
||||
/** يرسل الرمز عبر واتساب. phone بصيغة دولية بلا + (مثل 9627xxxxxxx). */
|
||||
async sendOtp(phone: string, code: string): Promise<void> {
|
||||
const token = await this.ensureToken();
|
||||
const res = await fetch(`${this.base}/api/otp/send`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
phone,
|
||||
type: this.config.get<string>('nabeh.otpType') ?? 'text',
|
||||
code,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '');
|
||||
throw new Error(`Nabeh otp/send failed: ${res.status} ${body}`);
|
||||
}
|
||||
this.logger.log(`OTP sent via WhatsApp to ${phone}`);
|
||||
}
|
||||
|
||||
private static jwtExp(token: string): number | null {
|
||||
try {
|
||||
const payload = JSON.parse(
|
||||
Buffer.from(token.split('.')[1], 'base64').toString('utf8'),
|
||||
);
|
||||
return typeof payload.exp === 'number' ? payload.exp : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user