mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat(payment-providers): implement Waafi in provider package
This commit is contained in:
@@ -39,12 +39,28 @@ export type {
|
||||
TelebirrTradeStatus,
|
||||
} from './providers/telebirr/telebirr.types';
|
||||
|
||||
// Waafi HPP request/response types (exported for apps that build/inspect requests directly)
|
||||
export type {
|
||||
WaafiState,
|
||||
WaafiHppPurchaseRequest,
|
||||
WaafiHppPurchaseResponse,
|
||||
WaafiGetTranInfoRequest,
|
||||
WaafiGetTranInfoResponse,
|
||||
} from './providers/waafi/waafi.types';
|
||||
|
||||
// Webhook payload types
|
||||
export type { TelebirrWebhookPayload } from './webhooks/telebirr-webhook.types';
|
||||
export type { CbeBirrWebhookPayload } from './webhooks/cbe-birr-webhook.types';
|
||||
export type { EBirrWebhookPayload } from './webhooks/ebirr-webhook.types';
|
||||
export type { CardWebhookPayload } from './webhooks/card-webhook.types';
|
||||
export type { WaafiWebhookPayload } from './webhooks/waafi-webhook.types';
|
||||
export type {
|
||||
WaafiWebhookPayload,
|
||||
WaafiWebhookTransactionPayload,
|
||||
WaafiWebhookTestPayload,
|
||||
WaafiWebhookHeaders,
|
||||
WaafiWebhookEvent,
|
||||
WaafiWebhookStatus,
|
||||
} from './webhooks/waafi-webhook.types';
|
||||
|
||||
// DI token for injecting all providers as an array (future multi-provider wiring)
|
||||
export const PAYMENT_PROVIDERS = Symbol('PAYMENT_PROVIDERS');
|
||||
|
||||
@@ -11,71 +11,18 @@ import {
|
||||
} from '@edr/types';
|
||||
import { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import * as crypto from 'node:crypto';
|
||||
import {
|
||||
WaafiGetTranInfoRequest,
|
||||
WaafiGetTranInfoResponse,
|
||||
WaafiHppPurchaseRequest,
|
||||
WaafiHppPurchaseResponse,
|
||||
} from './waafi.types';
|
||||
|
||||
const WAAFI_HTTP_TIMEOUT_MS = 10_000;
|
||||
|
||||
interface WaafiInitiateRequest {
|
||||
schemaVersion: string;
|
||||
requestId: string;
|
||||
timestamp: string;
|
||||
channelName: string;
|
||||
serviceName: string;
|
||||
serviceParams: {
|
||||
merchantUid: string;
|
||||
apiUserId: string;
|
||||
apiKey: string;
|
||||
paymentMethod: string;
|
||||
payerInfo: {
|
||||
accountNo: string;
|
||||
};
|
||||
transactionInfo: {
|
||||
referenceId: string;
|
||||
invoiceId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface WaafiInitiateResponse {
|
||||
responseCode: string;
|
||||
responseMsg: string;
|
||||
params?: {
|
||||
state: string;
|
||||
referenceId: string;
|
||||
transactionId: string;
|
||||
checkoutUrl?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface WaafiQueryRequest {
|
||||
schemaVersion: string;
|
||||
requestId: string;
|
||||
timestamp: string;
|
||||
channelName: string;
|
||||
serviceName: string;
|
||||
serviceParams: {
|
||||
merchantUid: string;
|
||||
apiUserId: string;
|
||||
apiKey: string;
|
||||
transactionId?: string;
|
||||
referenceId?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface WaafiQueryResponse {
|
||||
responseCode: string;
|
||||
responseMsg: string;
|
||||
params?: {
|
||||
state: string;
|
||||
referenceId: string;
|
||||
transactionId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
paidAmount?: number;
|
||||
};
|
||||
}
|
||||
const WAAFI_SUCCESS_CODE = '2001';
|
||||
/** Waafi cancels an unprocessed HPP session after ~5 minutes (RCS_HPP_USERACTION_TIMEOUT). */
|
||||
const WAAFI_HPP_SESSION_MS = 5 * 60_000;
|
||||
|
||||
@Injectable()
|
||||
export class WaafiProvider implements PaymentProvider {
|
||||
@@ -88,31 +35,30 @@ export class WaafiProvider implements PaymentProvider {
|
||||
) {}
|
||||
|
||||
async initiate(input: ProviderInitiationInput): Promise<ProviderInitiationResult> {
|
||||
const requestBody = this.buildInitiateRequest(input);
|
||||
const response = await this.postJson<WaafiInitiateResponse>(
|
||||
const requestBody = this.buildPurchaseRequest(input);
|
||||
const response = await this.postJson<WaafiHppPurchaseResponse>(
|
||||
`${this.baseUrl}/asm`,
|
||||
requestBody,
|
||||
);
|
||||
|
||||
if (response.responseCode !== '2001') {
|
||||
if (response.responseCode !== WAAFI_SUCCESS_CODE) {
|
||||
throw new Error(
|
||||
`Waafi initiate failed: ${response.responseCode} - ${response.responseMsg}`,
|
||||
`Waafi HPP_PURCHASE failed: responseCode=${response.responseCode} errorCode=${response.errorCode} msg=${response.responseMsg}`,
|
||||
);
|
||||
}
|
||||
|
||||
const transactionId = response.params?.transactionId;
|
||||
const checkoutUrl = response.params?.checkoutUrl || `${this.baseUrl}/checkout?ref=${transactionId}`;
|
||||
|
||||
if (!transactionId) {
|
||||
throw new Error(`Waafi returned no transactionId: ${JSON.stringify(response)}`);
|
||||
const checkoutUrl = response.params?.hppUrl ?? response.params?.directPaymentLink;
|
||||
const orderId = response.params?.orderId;
|
||||
if (!checkoutUrl || !orderId) {
|
||||
throw new Error(
|
||||
`Waafi HPP_PURCHASE succeeded but returned no hppUrl/orderId: ${JSON.stringify(response)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const expiresAt = new Date(Date.now() + 15 * 60_000); // 15 minutes
|
||||
|
||||
return {
|
||||
providerOrderId: transactionId,
|
||||
providerOrderId: orderId,
|
||||
clientAction: { type: 'REDIRECT', url: checkoutUrl },
|
||||
expiresAt,
|
||||
expiresAt: new Date(Date.now() + WAAFI_HPP_SESSION_MS),
|
||||
rawInitiation: {
|
||||
request: this.sanitize(requestBody),
|
||||
response,
|
||||
@@ -121,113 +67,144 @@ export class WaafiProvider implements PaymentProvider {
|
||||
}
|
||||
|
||||
async queryStatus(merchantOrderId: string): Promise<ProviderStatus> {
|
||||
const requestBody = this.buildQueryRequest(merchantOrderId);
|
||||
const response = await this.postJson<WaafiQueryResponse>(
|
||||
const requestBody = this.buildGetTranInfoRequest(merchantOrderId);
|
||||
const response = await this.postJson<WaafiGetTranInfoResponse>(
|
||||
`${this.baseUrl}/asm`,
|
||||
requestBody,
|
||||
);
|
||||
|
||||
const state = response.params?.state;
|
||||
const rawState = response.params?.status ?? response.params?.tranStatusDesc;
|
||||
const transactionId = response.params?.transactionId;
|
||||
const mapped = this.mapState(state);
|
||||
const mapped = this.mapStatus(rawState);
|
||||
|
||||
return {
|
||||
status: mapped,
|
||||
providerTxnId: transactionId,
|
||||
failureCode: mapped === ProviderPaymentStatus.FAILED && state ? state : undefined,
|
||||
failureCode:
|
||||
mapped === ProviderPaymentStatus.FAILED && rawState ? rawState : undefined,
|
||||
rawResponse: response as unknown as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
mapState(state: string | undefined): ProviderPaymentStatus {
|
||||
switch (state) {
|
||||
/** Map a webhook `payment.status` to the shared status enum. */
|
||||
mapWebhookStatus(status: string | undefined): ProviderPaymentStatus {
|
||||
return this.mapStatus(status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify an HMAC-SHA256 webhook signature.
|
||||
*
|
||||
* Signing string is `{timestamp}.{eventId}.{rawBody}` over the *raw* request body bytes — the
|
||||
* caller must pass the unparsed body string. Returns false (never throws) on any mismatch so
|
||||
* callers can treat verification as a boolean gate.
|
||||
*/
|
||||
verifyWebhookSignature(
|
||||
rawBody: string,
|
||||
signature: string | undefined,
|
||||
timestamp: string | undefined,
|
||||
eventId: string | undefined,
|
||||
): boolean {
|
||||
if (!this.webhookSecret) {
|
||||
this.logger.error('WAAFI_WEBHOOK_SECRET not configured; rejecting all webhooks');
|
||||
return false;
|
||||
}
|
||||
if (!signature || !timestamp || !eventId) {
|
||||
this.logger.warn('Waafi webhook missing signature/timestamp/event-id headers');
|
||||
return false;
|
||||
}
|
||||
|
||||
const signingString = `${timestamp}.${eventId}.${rawBody}`;
|
||||
const expected = crypto
|
||||
.createHmac('sha256', this.webhookSecret)
|
||||
.update(signingString)
|
||||
.digest('hex');
|
||||
|
||||
const provided = Buffer.from(signature, 'utf8');
|
||||
const computed = Buffer.from(expected, 'utf8');
|
||||
if (provided.length !== computed.length) return false;
|
||||
return crypto.timingSafeEqual(provided, computed);
|
||||
}
|
||||
|
||||
private mapStatus(raw: string | undefined): ProviderPaymentStatus {
|
||||
switch (raw?.toUpperCase()) {
|
||||
case 'APPROVED':
|
||||
case 'SUCCESS':
|
||||
return ProviderPaymentStatus.SUCCEEDED;
|
||||
case 'FAILED':
|
||||
case 'DECLINED':
|
||||
case 'CANCELED':
|
||||
case 'CANCELLED':
|
||||
return ProviderPaymentStatus.CANCELLED;
|
||||
case 'DECLINED':
|
||||
case 'FAILED':
|
||||
case 'EXPIRED':
|
||||
case 'TIMEOUT':
|
||||
return ProviderPaymentStatus.FAILED;
|
||||
case 'PENDING':
|
||||
case 'INITIATED':
|
||||
return ProviderPaymentStatus.REQUIRES_ACTION;
|
||||
case 'PROCESSING':
|
||||
return ProviderPaymentStatus.PROCESSING;
|
||||
default:
|
||||
return ProviderPaymentStatus.PROCESSING;
|
||||
}
|
||||
}
|
||||
|
||||
verifyWebhookSignature(payload: Record<string, unknown>): boolean {
|
||||
// Waafi webhook signature verification
|
||||
// Implementation depends on Waafi's webhook signature mechanism
|
||||
const signature = payload.signature as string;
|
||||
const apiKey = this.apiKey;
|
||||
|
||||
if (!signature || !apiKey) {
|
||||
this.logger.error('Waafi webhook missing signature or API key not configured');
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: Implement actual signature verification based on Waafi documentation
|
||||
// For now, basic validation
|
||||
return signature.length > 0;
|
||||
}
|
||||
|
||||
private buildInitiateRequest(input: ProviderInitiationInput): WaafiInitiateRequest {
|
||||
const amount = input.amountMinor / 100; // Convert minor units to major
|
||||
|
||||
private buildPurchaseRequest(input: ProviderInitiationInput): WaafiHppPurchaseRequest {
|
||||
return {
|
||||
schemaVersion: '1.0',
|
||||
requestId: this.generateRequestId(),
|
||||
timestamp: new Date().toISOString(),
|
||||
requestId: crypto.randomUUID(),
|
||||
timestamp: this.timestamp(),
|
||||
channelName: 'WEB',
|
||||
serviceName: 'API_PURCHASE',
|
||||
serviceName: 'HPP_PURCHASE',
|
||||
serviceParams: {
|
||||
merchantUid: this.merchantUid,
|
||||
apiUserId: this.apiUserId,
|
||||
apiKey: this.apiKey,
|
||||
paymentMethod: 'MWALLET_ACCOUNT',
|
||||
payerInfo: {
|
||||
accountNo: 'CUSTOMER', // Customer enters their number on Waafi page
|
||||
},
|
||||
storeId: this.storeId,
|
||||
hppKey: this.hppKey,
|
||||
paymentMethod: this.paymentMethod,
|
||||
hppSuccessCallbackUrl: this.successUrl,
|
||||
hppFailureCallbackUrl: this.failureUrl,
|
||||
hppRespDataFormat: this.respDataFormat,
|
||||
// MWALLET_ACCOUNT requires the payer phone up front; omit if the caller did not supply it
|
||||
// and let the hosted page collect it. See docs/waffi open question on payer-phone sourcing.
|
||||
...(input.payerAccount
|
||||
? { payerInfo: { subscriptionId: input.payerAccount } }
|
||||
: {}),
|
||||
transactionInfo: {
|
||||
referenceId: input.merchantOrderId,
|
||||
invoiceId: input.orderRef,
|
||||
amount,
|
||||
currency: input.currency === 'ETB' ? 'DJF' : input.currency, // Convert ETB to DJF
|
||||
amount: this.toAmount(input.amountMinor),
|
||||
// Waafi has no ETB; `waafi.currency` overrides the booking currency when set.
|
||||
currency: this.currency || input.currency,
|
||||
description: `EDR ${input.orderRef}`,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private buildQueryRequest(merchantOrderId: string): WaafiQueryRequest {
|
||||
private buildGetTranInfoRequest(merchantOrderId: string): WaafiGetTranInfoRequest {
|
||||
return {
|
||||
schemaVersion: '1.0',
|
||||
requestId: this.generateRequestId(),
|
||||
timestamp: new Date().toISOString(),
|
||||
requestId: crypto.randomUUID(),
|
||||
timestamp: this.timestamp(),
|
||||
channelName: 'WEB',
|
||||
serviceName: 'API_QUERY',
|
||||
serviceName: 'HPP_GETTRANINFO',
|
||||
serviceParams: {
|
||||
merchantUid: this.merchantUid,
|
||||
apiUserId: this.apiUserId,
|
||||
apiKey: this.apiKey,
|
||||
storeId: this.storeId,
|
||||
hppKey: this.hppKey,
|
||||
referenceId: merchantOrderId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private generateRequestId(): string {
|
||||
return `EDR-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
/** Convert integer minor units to a 2-decimal major amount (truncated, never rounded up). */
|
||||
private toAmount(amountMinor: number): number {
|
||||
return Math.trunc(amountMinor) / 100;
|
||||
}
|
||||
|
||||
private timestamp(): string {
|
||||
return Math.round(Date.now() / 1000).toString();
|
||||
}
|
||||
|
||||
private async postJson<T>(url: string, body: unknown): Promise<T> {
|
||||
const config: AxiosRequestConfig = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
timeout: WAAFI_HTTP_TIMEOUT_MS,
|
||||
};
|
||||
|
||||
@@ -252,24 +229,41 @@ export class WaafiProvider implements PaymentProvider {
|
||||
}
|
||||
}
|
||||
|
||||
private sanitize(body: WaafiInitiateRequest): Record<string, unknown> {
|
||||
const sanitized = { ...body };
|
||||
if (sanitized.serviceParams?.apiKey) {
|
||||
sanitized.serviceParams.apiKey = '***REDACTED***';
|
||||
}
|
||||
return sanitized as unknown as Record<string, unknown>;
|
||||
private sanitize(body: WaafiHppPurchaseRequest): Record<string, unknown> {
|
||||
return {
|
||||
...body,
|
||||
serviceParams: { ...body.serviceParams, hppKey: '***REDACTED***' },
|
||||
};
|
||||
}
|
||||
|
||||
private get baseUrl(): string {
|
||||
return this.config.get<string>('waafi.baseUrl') ?? 'https://api.waafipay.net';
|
||||
return this.config.get<string>('waafi.baseUrl') ?? 'https://sandbox.waafipay.com';
|
||||
}
|
||||
private get merchantUid(): string {
|
||||
return this.config.get<string>('waafi.merchantUid') ?? '';
|
||||
}
|
||||
private get apiUserId(): string {
|
||||
return this.config.get<string>('waafi.apiUserId') ?? '';
|
||||
private get storeId(): string {
|
||||
return this.config.get<string>('waafi.storeId') ?? '';
|
||||
}
|
||||
private get apiKey(): string {
|
||||
return this.config.get<string>('waafi.apiKey') ?? '';
|
||||
private get hppKey(): string {
|
||||
return this.config.get<string>('waafi.hppKey') ?? '';
|
||||
}
|
||||
private get webhookSecret(): string {
|
||||
return this.config.get<string>('waafi.webhookSecret') ?? '';
|
||||
}
|
||||
private get paymentMethod(): string {
|
||||
return this.config.get<string>('waafi.paymentMethod') ?? 'MWALLET_ACCOUNT';
|
||||
}
|
||||
private get currency(): string {
|
||||
return this.config.get<string>('waafi.currency') ?? '';
|
||||
}
|
||||
private get successUrl(): string {
|
||||
return this.config.get<string>('waafi.successUrl') ?? '';
|
||||
}
|
||||
private get failureUrl(): string {
|
||||
return this.config.get<string>('waafi.failureUrl') ?? '';
|
||||
}
|
||||
private get respDataFormat(): number {
|
||||
return this.config.get<number>('waafi.respDataFormat') ?? 1;
|
||||
}
|
||||
}
|
||||
|
||||
103
packages/payment-providers/src/providers/waafi/waafi.types.ts
Normal file
103
packages/payment-providers/src/providers/waafi/waafi.types.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
/**
|
||||
* WaafiPay (Hosted Payment Page) request/response types.
|
||||
*
|
||||
* WaafiPay multiplexes every operation through a single `POST /asm` endpoint, discriminated by
|
||||
* `serviceName`. We use the HPP family (`HPP_PURCHASE`, `HPP_GETTRANINFO`) which returns a hosted
|
||||
* redirect URL and supports webhooks — see docs/waffi/intro.md.
|
||||
*/
|
||||
|
||||
/** Terminal/intermediate transaction states reported by Waafi (sync `state` / `HPP_GETTRANINFO`). */
|
||||
export type WaafiState =
|
||||
| 'APPROVED'
|
||||
| 'DECLINED'
|
||||
| 'FAILED'
|
||||
| 'CANCELED'
|
||||
| 'EXPIRED'
|
||||
| 'TIMEOUT'
|
||||
| string;
|
||||
|
||||
/** Common request envelope shared by every `/asm` call. */
|
||||
export interface WaafiRequestEnvelope<TServiceParams> {
|
||||
schemaVersion: '1.0';
|
||||
requestId: string;
|
||||
timestamp: string;
|
||||
channelName: 'WEB';
|
||||
serviceName: string;
|
||||
serviceParams: TServiceParams;
|
||||
}
|
||||
|
||||
/** Common response envelope. `responseCode === '2001'` means the request was processed (not paid). */
|
||||
export interface WaafiResponseEnvelope<TParams> {
|
||||
schemaVersion: string;
|
||||
timestamp: string;
|
||||
responseId: string;
|
||||
responseCode: string;
|
||||
errorCode: string;
|
||||
responseMsg: string;
|
||||
params?: TParams;
|
||||
}
|
||||
|
||||
// --- HPP_PURCHASE -----------------------------------------------------------------------------
|
||||
|
||||
export interface WaafiHppPurchaseServiceParams {
|
||||
merchantUid: string;
|
||||
storeId: string;
|
||||
hppKey: string;
|
||||
paymentMethod: string;
|
||||
hppSuccessCallbackUrl: string;
|
||||
hppFailureCallbackUrl: string;
|
||||
/** Callback data format: 1 = POST, 2 = GET, 4 = Result Token. */
|
||||
hppRespDataFormat: number;
|
||||
/** Required for MWALLET_ACCOUNT — pre-fills (and locks) the payer's phone on the hosted page. */
|
||||
payerInfo?: {
|
||||
subscriptionId: string;
|
||||
};
|
||||
transactionInfo: {
|
||||
referenceId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
description?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type WaafiHppPurchaseRequest = WaafiRequestEnvelope<WaafiHppPurchaseServiceParams>;
|
||||
|
||||
export interface WaafiHppPurchaseParams {
|
||||
hppUrl: string;
|
||||
directPaymentLink?: string;
|
||||
orderId: string;
|
||||
referenceId: string;
|
||||
}
|
||||
|
||||
export type WaafiHppPurchaseResponse = WaafiResponseEnvelope<WaafiHppPurchaseParams>;
|
||||
|
||||
// --- HPP_GETTRANINFO --------------------------------------------------------------------------
|
||||
|
||||
export interface WaafiGetTranInfoServiceParams {
|
||||
merchantUid: string;
|
||||
storeId: string;
|
||||
hppKey: string;
|
||||
/** Either the merchant referenceId or the Waafi transactionId may be supplied. */
|
||||
referenceId?: string;
|
||||
transactionId?: string;
|
||||
}
|
||||
|
||||
export type WaafiGetTranInfoRequest = WaafiRequestEnvelope<WaafiGetTranInfoServiceParams>;
|
||||
|
||||
export interface WaafiGetTranInfoParams {
|
||||
tranStatusDesc?: string;
|
||||
amount?: string;
|
||||
payerId?: string;
|
||||
paymentMethod?: string;
|
||||
description?: string;
|
||||
tranDate?: string;
|
||||
currency?: string;
|
||||
invoiceId?: string;
|
||||
referenceId?: string;
|
||||
tranAmount?: string;
|
||||
transactionId?: string;
|
||||
tranStatusId?: string;
|
||||
status?: WaafiState;
|
||||
}
|
||||
|
||||
export type WaafiGetTranInfoResponse = WaafiResponseEnvelope<WaafiGetTranInfoParams>;
|
||||
@@ -1,15 +1,66 @@
|
||||
export interface WaafiWebhookPayload {
|
||||
schemaVersion: string;
|
||||
requestId: string;
|
||||
timestamp: string;
|
||||
eventType: string;
|
||||
params: {
|
||||
state: string;
|
||||
referenceId: string;
|
||||
transactionId: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
description?: string;
|
||||
};
|
||||
signature?: string;
|
||||
/**
|
||||
* WaafiPay webhook payloads (HPP authorization / refund / test).
|
||||
*
|
||||
* Webhooks are HMAC-SHA256 signed over `{timestamp}.{event_id}.{raw_body}` except `webhook.test`,
|
||||
* which is unsigned and only sent to validate endpoint reachability. See docs/waffi/intro.md.
|
||||
*/
|
||||
|
||||
export type WaafiWebhookEvent = 'authorization' | 'refund' | 'webhook.test';
|
||||
|
||||
export type WaafiWebhookStatus =
|
||||
| 'APPROVED'
|
||||
| 'FAILED'
|
||||
| 'DECLINED'
|
||||
| 'CANCELED'
|
||||
| 'EXPIRED'
|
||||
| 'TIMEOUT'
|
||||
| string;
|
||||
|
||||
/** Headers Waafi sends alongside signed webhooks (lowercased, as exposed by NestJS). */
|
||||
export interface WaafiWebhookHeaders {
|
||||
'x-webhook-timestamp'?: string;
|
||||
'x-webhook-event-id'?: string;
|
||||
'x-webhook-signature'?: string;
|
||||
'x-webhook-signature-alg'?: string;
|
||||
}
|
||||
|
||||
/** Nested payment object present on `authorization` and `refund` events. */
|
||||
export interface WaafiWebhookPayment {
|
||||
transaction_id: string;
|
||||
/** Present on authorization events (optional). */
|
||||
order_id?: string;
|
||||
transfer_code: string;
|
||||
amount: number;
|
||||
currency: string;
|
||||
/** Present on authorization events. */
|
||||
payment_method?: string;
|
||||
status: WaafiWebhookStatus;
|
||||
/** Our merchantOrderId. */
|
||||
reference_id: string;
|
||||
/** Present on authorization events. */
|
||||
channel?: string;
|
||||
description?: string;
|
||||
date: string;
|
||||
}
|
||||
|
||||
/** Unsigned validation ping sent on webhook registration/update. */
|
||||
export interface WaafiWebhookTestPayload {
|
||||
event: 'webhook.test';
|
||||
message?: string;
|
||||
merchant_uid: string;
|
||||
}
|
||||
|
||||
/** Real transaction notification (authorization or refund). */
|
||||
export interface WaafiWebhookTransactionPayload {
|
||||
event: 'authorization' | 'refund';
|
||||
merchant_id: number;
|
||||
merchant_uid: string;
|
||||
user_id: string;
|
||||
/** Authorization only. */
|
||||
customer_identity?: string;
|
||||
/** Authorization only (optional). */
|
||||
cardholder_name?: string;
|
||||
payment: WaafiWebhookPayment;
|
||||
}
|
||||
|
||||
export type WaafiWebhookPayload = WaafiWebhookTestPayload | WaafiWebhookTransactionPayload;
|
||||
|
||||
@@ -42,6 +42,12 @@ export interface ProviderInitiationInput {
|
||||
amountMinor: number;
|
||||
currency: string;
|
||||
platform?: PaymentPlatform;
|
||||
/**
|
||||
* Payer account identifier (e.g. mobile-wallet MSISDN in full international format).
|
||||
* Optional and provider-specific: some wallet providers (e.g. Waafi HPP with
|
||||
* MWALLET_ACCOUNT) require the payer's phone number up front to pre-fill the hosted page.
|
||||
*/
|
||||
payerAccount?: string;
|
||||
}
|
||||
|
||||
export interface ProviderInitiationResult {
|
||||
|
||||
Reference in New Issue
Block a user