mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat( waafi ): wire Waafi HPP into passenger API
This commit is contained in:
@@ -65,13 +65,25 @@ CARD_WEBHOOK_SECRET=
|
|||||||
CARD_WEBHOOK_URL=
|
CARD_WEBHOOK_URL=
|
||||||
CARD_RETURN_URL=
|
CARD_RETURN_URL=
|
||||||
|
|
||||||
# Waafi (Djibouti Mobile Money)
|
# Waafi (Djibouti Mobile Money — Hosted Payment Page)
|
||||||
WAAFI_BASE_URL=https://api.waafipay.net
|
# Sandbox: https://sandbox.waafipay.net | Production: https://api.waafipay.net
|
||||||
|
WAAFI_BASE_URL=https://sandbox.waafipay.net
|
||||||
WAAFI_MERCHANT_UID=
|
WAAFI_MERCHANT_UID=
|
||||||
WAAFI_API_USER_ID=
|
WAAFI_STORE_ID=
|
||||||
WAAFI_API_KEY=
|
WAAFI_HPP_KEY=
|
||||||
|
# HMAC secret returned once by WEBHOOK_REGISTER — verifies inbound webhooks
|
||||||
|
WAAFI_WEBHOOK_SECRET=
|
||||||
|
WAAFI_PAYMENT_METHOD=MWALLET_ACCOUNT
|
||||||
|
# Waafi has no ETB; overrides booking currency (USD/DJF/SLSH)
|
||||||
|
WAAFI_CURRENCY=DJF
|
||||||
|
WAAFI_HPP_SUCCESS_URL=
|
||||||
|
WAAFI_HPP_FAILURE_URL=
|
||||||
|
# 1 = POST, 2 = GET, 4 = Result Token
|
||||||
|
WAAFI_HPP_RESP_FORMAT=1
|
||||||
|
# Registered webhook URL (registration done out-of-band)
|
||||||
WAAFI_NOTIFY_URL=
|
WAAFI_NOTIFY_URL=
|
||||||
WAAFI_RETURN_URL=
|
# DEV ONLY — disable TLS cert verification (sandbox serves a *.waafi.com cert). Never true in prod.
|
||||||
|
WAAFI_INSECURE_TLS=false
|
||||||
|
|
||||||
# Payment Configuration
|
# Payment Configuration
|
||||||
PAYMENT_PROVIDERS_ENABLED=TELEBIRR,CBE_BIRR,EBIRR,CARD,WALLET,WAAFI
|
PAYMENT_PROVIDERS_ENABLED=TELEBIRR,CBE_BIRR,EBIRR,CARD,WALLET,WAAFI
|
||||||
|
|||||||
@@ -1,10 +1,27 @@
|
|||||||
import { registerAs } from '@nestjs/config';
|
import { registerAs } from '@nestjs/config';
|
||||||
|
|
||||||
export default registerAs('waafi', () => ({
|
export default registerAs('waafi', () => ({
|
||||||
baseUrl: process.env.WAAFI_BASE_URL ?? 'https://api.waafipay.net',
|
// `/asm` is appended in the provider; use sandbox by default, switch to
|
||||||
|
// https://api.waafipay.net in production.
|
||||||
|
baseUrl: process.env.WAAFI_BASE_URL ?? 'https://sandbox.waafipay.net',
|
||||||
|
// HPP credentials (Hosted Payment Page family).
|
||||||
merchantUid: process.env.WAAFI_MERCHANT_UID ?? '',
|
merchantUid: process.env.WAAFI_MERCHANT_UID ?? '',
|
||||||
apiUserId: process.env.WAAFI_API_USER_ID ?? '',
|
storeId: process.env.WAAFI_STORE_ID ?? '',
|
||||||
apiKey: process.env.WAAFI_API_KEY ?? '',
|
hppKey: process.env.WAAFI_HPP_KEY ?? '',
|
||||||
|
// HMAC secret returned once by WEBHOOK_REGISTER; verifies inbound webhooks.
|
||||||
|
webhookSecret: process.env.WAAFI_WEBHOOK_SECRET ?? '',
|
||||||
|
// Wallet payment method (EVC/ZAAD/Sahal) — MWALLET_ACCOUNT requires the payer phone up front.
|
||||||
|
paymentMethod: process.env.WAAFI_PAYMENT_METHOD ?? 'MWALLET_ACCOUNT',
|
||||||
|
// Waafi has no ETB; when set this overrides the booking currency (USD/DJF/SLSH).
|
||||||
|
currency: process.env.WAAFI_CURRENCY ?? 'DJF',
|
||||||
|
// Browser redirect targets after the hosted page completes/fails (UX only; webhook is source of truth).
|
||||||
|
successUrl: process.env.WAAFI_HPP_SUCCESS_URL ?? '',
|
||||||
|
failureUrl: process.env.WAAFI_HPP_FAILURE_URL ?? '',
|
||||||
|
// Callback data format: 1 = POST, 2 = GET, 4 = Result Token.
|
||||||
|
respDataFormat: Number(process.env.WAAFI_HPP_RESP_FORMAT ?? '1'),
|
||||||
|
// Registered webhook URL (reference only; registration is performed out-of-band).
|
||||||
notifyUrl: process.env.WAAFI_NOTIFY_URL ?? '',
|
notifyUrl: process.env.WAAFI_NOTIFY_URL ?? '',
|
||||||
returnUrl: process.env.WAAFI_RETURN_URL ?? '',
|
// DEV ONLY: disable TLS cert verification. The Waafi sandbox serves a *.waafi.com cert that
|
||||||
|
// does not match sandbox.waafipay.net (ERR_TLS_CERT_ALTNAME_INVALID). Never enable in prod.
|
||||||
|
insecureTls: process.env.WAAFI_INSECURE_TLS === 'true',
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { ResponseTransformInterceptor } from "./common/interceptors/response-tra
|
|||||||
import { SessionActivityInterceptor } from "./common/interceptors/session-activity.interceptor";
|
import { SessionActivityInterceptor } from "./common/interceptors/session-activity.interceptor";
|
||||||
|
|
||||||
async function bootstrap() {
|
async function bootstrap() {
|
||||||
const app = await NestFactory.create(AppModule);
|
// rawBody: true buffers the unparsed request body onto req.rawBody so webhook handlers
|
||||||
|
// (e.g. Waafi HMAC verification) can sign over the exact bytes the provider signed.
|
||||||
|
const app = await NestFactory.create(AppModule, { rawBody: true });
|
||||||
|
|
||||||
app.enableCors({
|
app.enableCors({
|
||||||
origin: [
|
origin: [
|
||||||
|
|||||||
@@ -1,93 +1,181 @@
|
|||||||
import { Injectable, Logger } from '@nestjs/common';
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { Prisma, PaymentIntentStatus, PaymentMethodType } from '@prisma/client';
|
||||||
import {
|
import {
|
||||||
WaafiProvider,
|
WaafiProvider,
|
||||||
WaafiWebhookPayload,
|
WaafiWebhookPayload,
|
||||||
|
WaafiWebhookHeaders,
|
||||||
|
WaafiWebhookTransactionPayload,
|
||||||
ProviderPaymentStatus,
|
ProviderPaymentStatus,
|
||||||
} from '@edr/payment-providers';
|
} from '@edr/payment-providers';
|
||||||
import { PaymentIntentStatus, PaymentMethodType } from '@prisma/client';
|
|
||||||
import { PrismaService } from '../../../common/prisma.service';
|
import { PrismaService } from '../../../common/prisma.service';
|
||||||
import { PaymentsService } from '../payments.service';
|
import { PaymentsService } from '../payments.service';
|
||||||
|
|
||||||
|
/** Reject webhooks whose timestamp is older than this (replay protection). */
|
||||||
|
const WAAFI_REPLAY_WINDOW_SECONDS = 300;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WaafiWebhookService {
|
export class WaafiWebhookService {
|
||||||
private readonly logger = new Logger(WaafiWebhookService.name);
|
private readonly logger = new Logger(WaafiWebhookService.name);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private prisma: PrismaService,
|
private readonly prisma: PrismaService,
|
||||||
private paymentsService: PaymentsService,
|
private readonly provider: WaafiProvider,
|
||||||
private waafiProvider: WaafiProvider,
|
private readonly payments: PaymentsService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async handleWebhook(payload: WaafiWebhookPayload): Promise<{ received: boolean }> {
|
async handleWebhook(
|
||||||
this.logger.log(
|
payload: WaafiWebhookPayload,
|
||||||
`Waafi webhook received: event=${payload.eventType} ref=${payload.params?.referenceId}`,
|
rawBody: string,
|
||||||
);
|
headers: WaafiWebhookHeaders,
|
||||||
|
): Promise<{ received: boolean }> {
|
||||||
|
|
||||||
const signatureValid = this.waafiProvider.verifyWebhookSignature(
|
console.log("Waafi Webhook Service");
|
||||||
payload as unknown as Record<string, unknown>,
|
// Unsigned validation ping sent on registration — acknowledge without verifying or persisting.
|
||||||
);
|
if (payload.event === 'webhook.test') {
|
||||||
|
this.logger.log('Waafi webhook.test ping received');
|
||||||
|
return { received: true };
|
||||||
|
}
|
||||||
|
|
||||||
const merchantOrderId = payload.params?.referenceId;
|
const { payment } = payload;
|
||||||
const transactionId = payload.params?.transactionId;
|
const merchantOrderId = payment.reference_id;
|
||||||
const state = payload.params?.state;
|
const providerTxnId = payment.transaction_id;
|
||||||
|
const eventId = headers['x-webhook-event-id'];
|
||||||
|
const timestamp = headers['x-webhook-timestamp'];
|
||||||
|
const signature = headers['x-webhook-signature'];
|
||||||
|
|
||||||
await this.prisma.paymentWebhookEvent.create({
|
const signatureValid =
|
||||||
data: {
|
this.isFresh(timestamp) &&
|
||||||
provider: PaymentMethodType.WAAFI,
|
this.provider.verifyWebhookSignature(rawBody, signature, timestamp, eventId);
|
||||||
externalEventId: payload.requestId,
|
|
||||||
merchantOrderId,
|
// X-Webhook-Event-Id is unique per event; fall back to a derived id if absent.
|
||||||
providerTxnId: transactionId,
|
const externalEventId = eventId ?? `${providerTxnId}_${payment.status}`;
|
||||||
signatureValid,
|
|
||||||
status: state || 'UNKNOWN',
|
const eventRow = await this.persistEvent({
|
||||||
payload: payload as any,
|
externalEventId,
|
||||||
},
|
merchantOrderId,
|
||||||
|
providerTxnId,
|
||||||
|
signatureValid,
|
||||||
|
status: payment.status,
|
||||||
|
payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!eventRow) {
|
||||||
|
this.logger.log(`Waafi webhook duplicate: ${externalEventId} — short-circuit OK`);
|
||||||
|
return { received: true };
|
||||||
|
}
|
||||||
|
|
||||||
if (!signatureValid) {
|
if (!signatureValid) {
|
||||||
this.logger.warn(`Waafi webhook signature invalid for ref=${merchantOrderId}`);
|
this.logger.warn(`Waafi webhook signature invalid/stale for ref=${merchantOrderId}`);
|
||||||
|
await this.markProcessed(eventRow.id, 'signature-invalid');
|
||||||
return { received: true };
|
return { received: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!merchantOrderId) {
|
const intent = await this.prisma.paymentIntent.findUnique({
|
||||||
this.logger.error('Waafi webhook missing referenceId');
|
|
||||||
return { received: true };
|
|
||||||
}
|
|
||||||
|
|
||||||
const intent = await this.prisma.paymentIntent.findFirst({
|
|
||||||
where: { merchantOrderId },
|
where: { merchantOrderId },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!intent) {
|
if (!intent) {
|
||||||
this.logger.warn(`No PaymentIntent found for merchantOrderId=${merchantOrderId}`);
|
this.logger.warn(`Waafi webhook: no PaymentIntent for ref=${merchantOrderId}`);
|
||||||
|
await this.markProcessed(eventRow.id, 'intent-not-found');
|
||||||
return { received: true };
|
return { received: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
const mappedStatus = this.waafiProvider.mapState(state);
|
const mapped = this.provider.mapWebhookStatus(payment.status);
|
||||||
|
|
||||||
if (mappedStatus === ProviderPaymentStatus.SUCCEEDED) {
|
try {
|
||||||
await this.paymentsService.finalizePaymentSuccess({
|
if (payload.event === 'refund') {
|
||||||
intentId: intent.id,
|
// Refund state is owned by PaymentsService.refund; just record the notification.
|
||||||
providerTxnId: transactionId,
|
this.logger.log(
|
||||||
});
|
`Waafi refund webhook for ref=${merchantOrderId} status=${payment.status}`,
|
||||||
this.logger.log(`Waafi payment succeeded: intent=${intent.id} txn=${transactionId}`);
|
);
|
||||||
} else if (mappedStatus === ProviderPaymentStatus.FAILED) {
|
} else if (mapped === ProviderPaymentStatus.SUCCEEDED) {
|
||||||
await this.paymentsService.markPaymentFailed({
|
await this.payments.finalizePaymentSuccess({
|
||||||
intentId: intent.id,
|
intentId: intent.id,
|
||||||
failureCode: state,
|
providerTxnId,
|
||||||
failureMessage: payload.params?.description,
|
paidAt: this.parseDate(payment.date),
|
||||||
});
|
});
|
||||||
this.logger.log(`Waafi payment failed: intent=${intent.id} state=${state}`);
|
} else if (
|
||||||
} else {
|
mapped === ProviderPaymentStatus.FAILED ||
|
||||||
await this.prisma.paymentIntent.update({
|
mapped === ProviderPaymentStatus.CANCELLED
|
||||||
where: { id: intent.id },
|
) {
|
||||||
data: {
|
await this.payments.markPaymentFailed({
|
||||||
status: mappedStatus as unknown as PaymentIntentStatus,
|
intentId: intent.id,
|
||||||
providerTxnId: transactionId,
|
failureCode: payment.status,
|
||||||
},
|
failureMessage: payment.description,
|
||||||
});
|
});
|
||||||
this.logger.log(`Waafi payment status updated: intent=${intent.id} status=${mappedStatus}`);
|
} else {
|
||||||
|
await this.prisma.paymentIntent.update({
|
||||||
|
where: { id: intent.id },
|
||||||
|
data: {
|
||||||
|
status: mapped as unknown as PaymentIntentStatus,
|
||||||
|
providerTxnId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await this.markProcessed(eventRow.id);
|
||||||
|
} catch (err) {
|
||||||
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
|
this.logger.error(
|
||||||
|
`Waafi webhook processing failed for ${merchantOrderId}: ${message}`,
|
||||||
|
);
|
||||||
|
await this.markProcessed(eventRow.id, `processing-error: ${message}`);
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
|
|
||||||
return { received: true };
|
return { received: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async persistEvent(input: {
|
||||||
|
externalEventId: string;
|
||||||
|
merchantOrderId: string;
|
||||||
|
providerTxnId?: string;
|
||||||
|
signatureValid: boolean;
|
||||||
|
status: string;
|
||||||
|
payload: WaafiWebhookTransactionPayload;
|
||||||
|
}): Promise<{ id: string } | null> {
|
||||||
|
try {
|
||||||
|
return await this.prisma.paymentWebhookEvent.create({
|
||||||
|
data: {
|
||||||
|
provider: PaymentMethodType.WAAFI,
|
||||||
|
externalEventId: input.externalEventId,
|
||||||
|
merchantOrderId: input.merchantOrderId,
|
||||||
|
providerTxnId: input.providerTxnId,
|
||||||
|
signatureValid: input.signatureValid,
|
||||||
|
status: input.status,
|
||||||
|
payload: input.payload as unknown as Prisma.InputJsonValue,
|
||||||
|
},
|
||||||
|
select: { id: true },
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
if (
|
||||||
|
err instanceof Prisma.PrismaClientKnownRequestError &&
|
||||||
|
err.code === 'P2002'
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async markProcessed(eventId: string, processingError?: string): Promise<void> {
|
||||||
|
await this.prisma.paymentWebhookEvent.update({
|
||||||
|
where: { id: eventId },
|
||||||
|
data: { processedAt: new Date(), processingError },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when the webhook timestamp (unix seconds) is within the replay window. */
|
||||||
|
private isFresh(timestamp: string | undefined): boolean {
|
||||||
|
if (!timestamp) return false;
|
||||||
|
const ts = parseInt(timestamp, 10);
|
||||||
|
if (Number.isNaN(ts)) return false;
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
return Math.abs(now - ts) <= WAAFI_REPLAY_WINDOW_SECONDS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Parse Waafi's "YYYY-MM-DD HH:mm:ss" payment date; undefined when unparseable. */
|
||||||
|
private parseDate(raw: string | undefined): Date | undefined {
|
||||||
|
if (!raw) return undefined;
|
||||||
|
const d = new Date(raw);
|
||||||
|
return Number.isNaN(d.getTime()) ? undefined : d;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import {All, Body, Controller, Headers, HttpCode, HttpStatus, Logger, Post} from '@nestjs/common';
|
import {All, Body, Controller, Headers, HttpCode, HttpStatus, Logger, Post, Req} from '@nestjs/common';
|
||||||
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
import { ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||||
import {
|
import {
|
||||||
TelebirrWebhookPayload,
|
TelebirrWebhookPayload,
|
||||||
CbeBirrWebhookPayload,
|
CbeBirrWebhookPayload,
|
||||||
EBirrWebhookPayload,
|
EBirrWebhookPayload,
|
||||||
CardWebhookPayload,
|
CardWebhookPayload,
|
||||||
|
WaafiWebhookPayload,
|
||||||
|
WaafiWebhookHeaders,
|
||||||
} from '@edr/payment-providers';
|
} from '@edr/payment-providers';
|
||||||
import { TelebirrWebhookService } from './telebirr-webhook.service';
|
import { TelebirrWebhookService } from './telebirr-webhook.service';
|
||||||
import { CbeBirrWebhookService } from './cbe-birr-webhook.service';
|
import { CbeBirrWebhookService } from './cbe-birr-webhook.service';
|
||||||
@@ -103,9 +105,18 @@ export class WebhooksController {
|
|||||||
summary: 'Waafi payment notification callback (Djibouti)',
|
summary: 'Waafi payment notification callback (Djibouti)',
|
||||||
description: 'Webhook endpoint for Waafi mobile money payment status updates. Used by Djiboutian passengers.'
|
description: 'Webhook endpoint for Waafi mobile money payment status updates. Used by Djiboutian passengers.'
|
||||||
})
|
})
|
||||||
async receiveWaafi(@Body() payload: any) {
|
async receiveWaafi(
|
||||||
|
@Body() payload: WaafiWebhookPayload,
|
||||||
|
@Headers() headers: WaafiWebhookHeaders,
|
||||||
|
@Req() req: { rawBody?: Buffer },
|
||||||
|
) {
|
||||||
|
this.logger.log(
|
||||||
|
`Waafi webhook hit: event=${payload?.event ?? 'unknown'} eventId=${headers['x-webhook-event-id'] ?? 'n/a'}`,
|
||||||
|
);
|
||||||
try {
|
try {
|
||||||
await this.waafi.handleWebhook(payload);
|
// HMAC verification must sign over the exact raw bytes Waafi sent, not re-serialized JSON.
|
||||||
|
const rawBody = req.rawBody?.toString('utf8') ?? '';
|
||||||
|
await this.waafi.handleWebhook(payload, rawBody, headers);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const message = err instanceof Error ? err.message : String(err);
|
const message = err instanceof Error ? err.message : String(err);
|
||||||
this.logger.error(`Waafi webhook handler threw: ${message}`);
|
this.logger.error(`Waafi webhook handler threw: ${message}`);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
import { AxiosError, AxiosRequestConfig } from 'axios';
|
import { AxiosError, AxiosRequestConfig } from 'axios';
|
||||||
import { firstValueFrom } from 'rxjs';
|
import { firstValueFrom } from 'rxjs';
|
||||||
import * as crypto from 'node:crypto';
|
import * as crypto from 'node:crypto';
|
||||||
|
import * as https from 'node:https';
|
||||||
import {
|
import {
|
||||||
WaafiGetTranInfoRequest,
|
WaafiGetTranInfoRequest,
|
||||||
WaafiGetTranInfoResponse,
|
WaafiGetTranInfoResponse,
|
||||||
@@ -28,11 +29,20 @@ const WAAFI_HPP_SESSION_MS = 5 * 60_000;
|
|||||||
export class WaafiProvider implements PaymentProvider {
|
export class WaafiProvider implements PaymentProvider {
|
||||||
readonly method = ProviderMethod.WAAFI;
|
readonly method = ProviderMethod.WAAFI;
|
||||||
private readonly logger = new Logger(WaafiProvider.name);
|
private readonly logger = new Logger(WaafiProvider.name);
|
||||||
|
private readonly httpsAgent: https.Agent;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly config: ConfigService,
|
private readonly config: ConfigService,
|
||||||
private readonly http: HttpService,
|
private readonly http: HttpService,
|
||||||
) {}
|
) {
|
||||||
|
const insecure = this.config.get<boolean>('waafi.insecureTls');
|
||||||
|
if (insecure) {
|
||||||
|
this.logger.warn(
|
||||||
|
'WAAFI_INSECURE_TLS=true — TLS verification disabled for Waafi calls. DEV ONLY.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.httpsAgent = new https.Agent({ rejectUnauthorized: !insecure });
|
||||||
|
}
|
||||||
|
|
||||||
async initiate(input: ProviderInitiationInput): Promise<ProviderInitiationResult> {
|
async initiate(input: ProviderInitiationInput): Promise<ProviderInitiationResult> {
|
||||||
const requestBody = this.buildPurchaseRequest(input);
|
const requestBody = this.buildPurchaseRequest(input);
|
||||||
@@ -206,6 +216,7 @@ export class WaafiProvider implements PaymentProvider {
|
|||||||
const config: AxiosRequestConfig = {
|
const config: AxiosRequestConfig = {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
timeout: WAAFI_HTTP_TIMEOUT_MS,
|
timeout: WAAFI_HTTP_TIMEOUT_MS,
|
||||||
|
httpsAgent: this.httpsAgent,
|
||||||
};
|
};
|
||||||
|
|
||||||
const started = Date.now();
|
const started = Date.now();
|
||||||
@@ -237,7 +248,7 @@ export class WaafiProvider implements PaymentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private get baseUrl(): string {
|
private get baseUrl(): string {
|
||||||
return this.config.get<string>('waafi.baseUrl') ?? 'https://sandbox.waafipay.com';
|
return this.config.get<string>('waafi.baseUrl') ?? 'https://sandbox.waafipay.net';
|
||||||
}
|
}
|
||||||
private get merchantUid(): string {
|
private get merchantUid(): string {
|
||||||
return this.config.get<string>('waafi.merchantUid') ?? '';
|
return this.config.get<string>('waafi.merchantUid') ?? '';
|
||||||
|
|||||||
Reference in New Issue
Block a user