mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 07:08:18 +00:00
feat(payment-providers): implement Waafi in provider package
This commit is contained in:
@@ -39,12 +39,28 @@ export type {
|
|||||||
TelebirrTradeStatus,
|
TelebirrTradeStatus,
|
||||||
} from './providers/telebirr/telebirr.types';
|
} 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
|
// Webhook payload types
|
||||||
export type { TelebirrWebhookPayload } from './webhooks/telebirr-webhook.types';
|
export type { TelebirrWebhookPayload } from './webhooks/telebirr-webhook.types';
|
||||||
export type { CbeBirrWebhookPayload } from './webhooks/cbe-birr-webhook.types';
|
export type { CbeBirrWebhookPayload } from './webhooks/cbe-birr-webhook.types';
|
||||||
export type { EBirrWebhookPayload } from './webhooks/ebirr-webhook.types';
|
export type { EBirrWebhookPayload } from './webhooks/ebirr-webhook.types';
|
||||||
export type { CardWebhookPayload } from './webhooks/card-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)
|
// DI token for injecting all providers as an array (future multi-provider wiring)
|
||||||
export const PAYMENT_PROVIDERS = Symbol('PAYMENT_PROVIDERS');
|
export const PAYMENT_PROVIDERS = Symbol('PAYMENT_PROVIDERS');
|
||||||
|
|||||||
@@ -11,71 +11,18 @@ import {
|
|||||||
} from '@edr/types';
|
} from '@edr/types';
|
||||||
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 {
|
||||||
|
WaafiGetTranInfoRequest,
|
||||||
|
WaafiGetTranInfoResponse,
|
||||||
|
WaafiHppPurchaseRequest,
|
||||||
|
WaafiHppPurchaseResponse,
|
||||||
|
} from './waafi.types';
|
||||||
|
|
||||||
const WAAFI_HTTP_TIMEOUT_MS = 10_000;
|
const WAAFI_HTTP_TIMEOUT_MS = 10_000;
|
||||||
|
const WAAFI_SUCCESS_CODE = '2001';
|
||||||
interface WaafiInitiateRequest {
|
/** Waafi cancels an unprocessed HPP session after ~5 minutes (RCS_HPP_USERACTION_TIMEOUT). */
|
||||||
schemaVersion: string;
|
const WAAFI_HPP_SESSION_MS = 5 * 60_000;
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WaafiProvider implements PaymentProvider {
|
export class WaafiProvider implements PaymentProvider {
|
||||||
@@ -88,31 +35,30 @@ export class WaafiProvider implements PaymentProvider {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async initiate(input: ProviderInitiationInput): Promise<ProviderInitiationResult> {
|
async initiate(input: ProviderInitiationInput): Promise<ProviderInitiationResult> {
|
||||||
const requestBody = this.buildInitiateRequest(input);
|
const requestBody = this.buildPurchaseRequest(input);
|
||||||
const response = await this.postJson<WaafiInitiateResponse>(
|
const response = await this.postJson<WaafiHppPurchaseResponse>(
|
||||||
`${this.baseUrl}/asm`,
|
`${this.baseUrl}/asm`,
|
||||||
requestBody,
|
requestBody,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.responseCode !== '2001') {
|
if (response.responseCode !== WAAFI_SUCCESS_CODE) {
|
||||||
throw new Error(
|
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?.hppUrl ?? response.params?.directPaymentLink;
|
||||||
const checkoutUrl = response.params?.checkoutUrl || `${this.baseUrl}/checkout?ref=${transactionId}`;
|
const orderId = response.params?.orderId;
|
||||||
|
if (!checkoutUrl || !orderId) {
|
||||||
if (!transactionId) {
|
throw new Error(
|
||||||
throw new Error(`Waafi returned no transactionId: ${JSON.stringify(response)}`);
|
`Waafi HPP_PURCHASE succeeded but returned no hppUrl/orderId: ${JSON.stringify(response)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const expiresAt = new Date(Date.now() + 15 * 60_000); // 15 minutes
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
providerOrderId: transactionId,
|
providerOrderId: orderId,
|
||||||
clientAction: { type: 'REDIRECT', url: checkoutUrl },
|
clientAction: { type: 'REDIRECT', url: checkoutUrl },
|
||||||
expiresAt,
|
expiresAt: new Date(Date.now() + WAAFI_HPP_SESSION_MS),
|
||||||
rawInitiation: {
|
rawInitiation: {
|
||||||
request: this.sanitize(requestBody),
|
request: this.sanitize(requestBody),
|
||||||
response,
|
response,
|
||||||
@@ -121,113 +67,144 @@ export class WaafiProvider implements PaymentProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async queryStatus(merchantOrderId: string): Promise<ProviderStatus> {
|
async queryStatus(merchantOrderId: string): Promise<ProviderStatus> {
|
||||||
const requestBody = this.buildQueryRequest(merchantOrderId);
|
const requestBody = this.buildGetTranInfoRequest(merchantOrderId);
|
||||||
const response = await this.postJson<WaafiQueryResponse>(
|
const response = await this.postJson<WaafiGetTranInfoResponse>(
|
||||||
`${this.baseUrl}/asm`,
|
`${this.baseUrl}/asm`,
|
||||||
requestBody,
|
requestBody,
|
||||||
);
|
);
|
||||||
|
|
||||||
const state = response.params?.state;
|
const rawState = response.params?.status ?? response.params?.tranStatusDesc;
|
||||||
const transactionId = response.params?.transactionId;
|
const transactionId = response.params?.transactionId;
|
||||||
const mapped = this.mapState(state);
|
const mapped = this.mapStatus(rawState);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
status: mapped,
|
status: mapped,
|
||||||
providerTxnId: transactionId,
|
providerTxnId: transactionId,
|
||||||
failureCode: mapped === ProviderPaymentStatus.FAILED && state ? state : undefined,
|
failureCode:
|
||||||
|
mapped === ProviderPaymentStatus.FAILED && rawState ? rawState : undefined,
|
||||||
rawResponse: response as unknown as Record<string, unknown>,
|
rawResponse: response as unknown as Record<string, unknown>,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
mapState(state: string | undefined): ProviderPaymentStatus {
|
/** Map a webhook `payment.status` to the shared status enum. */
|
||||||
switch (state) {
|
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 'APPROVED':
|
||||||
case 'SUCCESS':
|
case 'SUCCESS':
|
||||||
return ProviderPaymentStatus.SUCCEEDED;
|
return ProviderPaymentStatus.SUCCEEDED;
|
||||||
case 'FAILED':
|
case 'CANCELED':
|
||||||
case 'DECLINED':
|
|
||||||
case 'CANCELLED':
|
case 'CANCELLED':
|
||||||
|
return ProviderPaymentStatus.CANCELLED;
|
||||||
|
case 'DECLINED':
|
||||||
|
case 'FAILED':
|
||||||
case 'EXPIRED':
|
case 'EXPIRED':
|
||||||
|
case 'TIMEOUT':
|
||||||
return ProviderPaymentStatus.FAILED;
|
return ProviderPaymentStatus.FAILED;
|
||||||
case 'PENDING':
|
case 'PENDING':
|
||||||
case 'INITIATED':
|
case 'INITIATED':
|
||||||
return ProviderPaymentStatus.REQUIRES_ACTION;
|
return ProviderPaymentStatus.REQUIRES_ACTION;
|
||||||
case 'PROCESSING':
|
|
||||||
return ProviderPaymentStatus.PROCESSING;
|
|
||||||
default:
|
default:
|
||||||
return ProviderPaymentStatus.PROCESSING;
|
return ProviderPaymentStatus.PROCESSING;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
verifyWebhookSignature(payload: Record<string, unknown>): boolean {
|
private buildPurchaseRequest(input: ProviderInitiationInput): WaafiHppPurchaseRequest {
|
||||||
// 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
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
schemaVersion: '1.0',
|
schemaVersion: '1.0',
|
||||||
requestId: this.generateRequestId(),
|
requestId: crypto.randomUUID(),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: this.timestamp(),
|
||||||
channelName: 'WEB',
|
channelName: 'WEB',
|
||||||
serviceName: 'API_PURCHASE',
|
serviceName: 'HPP_PURCHASE',
|
||||||
serviceParams: {
|
serviceParams: {
|
||||||
merchantUid: this.merchantUid,
|
merchantUid: this.merchantUid,
|
||||||
apiUserId: this.apiUserId,
|
storeId: this.storeId,
|
||||||
apiKey: this.apiKey,
|
hppKey: this.hppKey,
|
||||||
paymentMethod: 'MWALLET_ACCOUNT',
|
paymentMethod: this.paymentMethod,
|
||||||
payerInfo: {
|
hppSuccessCallbackUrl: this.successUrl,
|
||||||
accountNo: 'CUSTOMER', // Customer enters their number on Waafi page
|
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: {
|
transactionInfo: {
|
||||||
referenceId: input.merchantOrderId,
|
referenceId: input.merchantOrderId,
|
||||||
invoiceId: input.orderRef,
|
amount: this.toAmount(input.amountMinor),
|
||||||
amount,
|
// Waafi has no ETB; `waafi.currency` overrides the booking currency when set.
|
||||||
currency: input.currency === 'ETB' ? 'DJF' : input.currency, // Convert ETB to DJF
|
currency: this.currency || input.currency,
|
||||||
description: `EDR ${input.orderRef}`,
|
description: `EDR ${input.orderRef}`,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildQueryRequest(merchantOrderId: string): WaafiQueryRequest {
|
private buildGetTranInfoRequest(merchantOrderId: string): WaafiGetTranInfoRequest {
|
||||||
return {
|
return {
|
||||||
schemaVersion: '1.0',
|
schemaVersion: '1.0',
|
||||||
requestId: this.generateRequestId(),
|
requestId: crypto.randomUUID(),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: this.timestamp(),
|
||||||
channelName: 'WEB',
|
channelName: 'WEB',
|
||||||
serviceName: 'API_QUERY',
|
serviceName: 'HPP_GETTRANINFO',
|
||||||
serviceParams: {
|
serviceParams: {
|
||||||
merchantUid: this.merchantUid,
|
merchantUid: this.merchantUid,
|
||||||
apiUserId: this.apiUserId,
|
storeId: this.storeId,
|
||||||
apiKey: this.apiKey,
|
hppKey: this.hppKey,
|
||||||
referenceId: merchantOrderId,
|
referenceId: merchantOrderId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private generateRequestId(): string {
|
/** Convert integer minor units to a 2-decimal major amount (truncated, never rounded up). */
|
||||||
return `EDR-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
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> {
|
private async postJson<T>(url: string, body: unknown): Promise<T> {
|
||||||
const config: AxiosRequestConfig = {
|
const config: AxiosRequestConfig = {
|
||||||
headers: {
|
headers: { 'Content-Type': 'application/json' },
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
timeout: WAAFI_HTTP_TIMEOUT_MS,
|
timeout: WAAFI_HTTP_TIMEOUT_MS,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -252,24 +229,41 @@ export class WaafiProvider implements PaymentProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private sanitize(body: WaafiInitiateRequest): Record<string, unknown> {
|
private sanitize(body: WaafiHppPurchaseRequest): Record<string, unknown> {
|
||||||
const sanitized = { ...body };
|
return {
|
||||||
if (sanitized.serviceParams?.apiKey) {
|
...body,
|
||||||
sanitized.serviceParams.apiKey = '***REDACTED***';
|
serviceParams: { ...body.serviceParams, hppKey: '***REDACTED***' },
|
||||||
}
|
};
|
||||||
return sanitized as unknown as Record<string, unknown>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private get baseUrl(): string {
|
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 {
|
private get merchantUid(): string {
|
||||||
return this.config.get<string>('waafi.merchantUid') ?? '';
|
return this.config.get<string>('waafi.merchantUid') ?? '';
|
||||||
}
|
}
|
||||||
private get apiUserId(): string {
|
private get storeId(): string {
|
||||||
return this.config.get<string>('waafi.apiUserId') ?? '';
|
return this.config.get<string>('waafi.storeId') ?? '';
|
||||||
}
|
}
|
||||||
private get apiKey(): string {
|
private get hppKey(): string {
|
||||||
return this.config.get<string>('waafi.apiKey') ?? '';
|
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;
|
* WaafiPay webhook payloads (HPP authorization / refund / test).
|
||||||
requestId: string;
|
*
|
||||||
timestamp: string;
|
* Webhooks are HMAC-SHA256 signed over `{timestamp}.{event_id}.{raw_body}` except `webhook.test`,
|
||||||
eventType: string;
|
* which is unsigned and only sent to validate endpoint reachability. See docs/waffi/intro.md.
|
||||||
params: {
|
*/
|
||||||
state: string;
|
|
||||||
referenceId: string;
|
export type WaafiWebhookEvent = 'authorization' | 'refund' | 'webhook.test';
|
||||||
transactionId: string;
|
|
||||||
amount: number;
|
export type WaafiWebhookStatus =
|
||||||
currency: string;
|
| 'APPROVED'
|
||||||
description?: string;
|
| 'FAILED'
|
||||||
};
|
| 'DECLINED'
|
||||||
signature?: string;
|
| '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;
|
amountMinor: number;
|
||||||
currency: string;
|
currency: string;
|
||||||
platform?: PaymentPlatform;
|
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 {
|
export interface ProviderInitiationResult {
|
||||||
|
|||||||
Reference in New Issue
Block a user