📍 المقالة الرئيسية للمجموعة: Stack لوجستية 2026.
40% من زبائن إفريقيا الغربية يفضلون Wave Mobile Money للدفع. أرخص (1% عمولة)، فوري، آمن، مدمج في الهاتف. دمج Wave في tunnel checkout متجر + تطبيق ساعي = تخفيض refus payment، إدارة COD أبسط، reconciliation يومية مؤتمتة. هذا الدرس يفصل التكامل الكامل: Wave Business API، QR code dynamique، webhooks signed، reconciliation.
المتطلبات
حساب Wave Business (تسجيل على business.wave.com، KYC 3-5 أيام). متجر إلكتروني (WooCommerce، Next.js، QloApps). تطبيق ساعي. backend Node.js/Bun. المستوى المتوقع: متقدم. الوقت المقدر: 4-6 ساعات.
الخطوة 1 — الحصول على API credentials
business.wave.com → Developers → API Keys. تستلم:
WAVE_API_KEY: للمصادقة API.WAVE_WEBHOOK_SECRET: للتحقق من signature webhooks.WAVE_BUSINESS_ID: معرف نشاطك التجاري.
خزن في Vaultwarden + متغيرات بيئة. لا تكشف API key في front-end أو git.
الخطوة 2 — إنشاء checkout session
عند checkout، backend ينشئ Wave session ويعيد URL إعادة توجيه. العميل ينقر، يفتح Wave app أو موقع، يدفع، يعود.
// backend/src/payment/wave.ts
import { createHmac } from 'node:crypto';
export async function createWaveCheckoutSession(orderId: string, amount: number, customerPhone: string) {
const response = await fetch('https://api.wave.com/v1/checkout/sessions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.WAVE_API_KEY}`,
'Content-Type': 'application/json',
'idempotency-key': `order-${orderId}`
},
body: JSON.stringify({
amount: amount.toString(),
currency: 'XOF',
success_url: `https://shop.com/order/${orderId}/success`,
error_url: `https://shop.com/order/${orderId}/error`,
client_reference: orderId,
restrict_payer_mobile: customerPhone // اختياري: قيد على رقم محدد
})
});
if (!response.ok) {
throw new Error(`Wave error: ${await response.text()}`);
}
const session = await response.json();
// {
// id: 'cos-1...',
// wave_launch_url: 'https://pay.wave.com/c/...',
// amount: '5000',
// currency: 'XOF',
// status: 'open'
// }
return session;
}
الخطوة 3 — Frontend تكامل
زر «دفع بـ Wave» يستدعي backend، يحول العميل إلى wave_launch_url.
// app/checkout/page.tsx
async function payWithWave() {
const res = await fetch('/api/checkout', {
method: 'POST',
body: JSON.stringify({
items: cart,
paymentMethod: 'wave',
customerPhone
})
});
const { wave_launch_url } = await res.json();
// إعادة توجيه إلى Wave
window.location.href = wave_launch_url;
// أو فتح في tab جديد لـ desktop:
// window.open(wave_launch_url, '_blank');
}
الخطوة 4 — Webhook استقبال التأكيدات
Wave يرسل webhook عندما الدفع ناجح أو فشل. التحقق من signature إلزامي لتجنب spam.
// backend/src/webhooks/wave.ts
import express from 'express';
app.post('/api/webhooks/wave', express.raw({ type: 'application/json' }), async (req, res) => {
const signature = req.header('Wave-Signature');
const payload = req.body; // raw bytes
// التحقق HMAC
const expectedSig = createHmac('sha256', process.env.WAVE_WEBHOOK_SECRET!)
.update(payload)
.digest('hex');
if (signature !== expectedSig) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(payload.toString());
// {
// id: 'evt-1...',
// type: 'checkout.session.completed',
// data: {
// id: 'cos-1...',
// amount: '5000',
// currency: 'XOF',
// client_reference: 'order-123',
// payment_status: 'succeeded',
// payer_phone: '+221701234567'
// }
// }
switch (event.type) {
case 'checkout.session.completed':
await handlePaymentSuccess(event.data);
break;
case 'checkout.session.payment_failed':
await handlePaymentFailure(event.data);
break;
}
res.status(200).send('OK');
});
async function handlePaymentSuccess(session: any) {
await sql`
UPDATE orders
SET status = 'paid',
payment_method = 'wave',
paid_at = now(),
wave_session_id = ${session.id}
WHERE id = ${session.client_reference}
`;
// إشعار العميل
await sendSMS(session.payer_phone, 'تم استلام دفعتك. طلبك في طور التحضير.');
// إرسال إلى الساعي (إذا التوصيل تلقائي)
await assignDeliveryToCourier(session.client_reference);
}
الخطوة 5 — حالة Cash on Delivery
40% من الزبائن يدفعون نقداً عند التوصيل. الساعي يقبض، يعطي إيصال. التطبيق يسجل المبلغ، reconciliation يومية.
// تطبيق ساعي
const confirmCashPayment = async (deliveryId: string, amount: number) => {
await db.deliveries.update(deliveryId, {
status: 'delivered',
paymentMethod: 'cash',
cashAmount: amount,
syncStatus: 'pending',
updatedAt: Date.now()
});
await db.syncQueue.add({
type: 'cash_payment',
data: { deliveryId, amount, courierId: getMe().id },
attempts: 0,
createdAt: Date.now()
});
};
الخطوة 6 — QR Code للساعي
الساعي يولد QR code dynamique للتوصيل، العميل يمسح، يدفع مباشرة على الساعي. يتطلب Wave Personal API (تحويل بين Wave wallets).
// تطبيق ساعي - توليد QR
import QRCode from 'qrcode';
const generatePaymentQR = async (deliveryId: string, amount: number) => {
// إنشاء payment link قصير
const res = await fetch('/api/wave/payment-link', {
method: 'POST',
body: JSON.stringify({ deliveryId, amount })
});
const { paymentUrl } = await res.json();
// تحويل إلى QR code
const qrDataUrl = await QRCode.toDataURL(paymentUrl);
return qrDataUrl;
};
// عرض QR
<img src={qrDataUrl} alt="مسح للدفع" />
<p>المبلغ: {amount} XOF</p>
<p>أو ادفع نقداً</p>
الخطوة 7 — Reconciliation يومية
كل ليلة، script يقارن الدفعات Wave مع الطلبات في DB. يكشف الانحرافات (دفعة Wave دون طلب، طلب نقداً دون دفعة، إلخ).
// scripts/wave-reconciliation.ts
async function reconcile(date: string) {
// جلب كل الـ Wave transactions لليوم
const waveTransactions = await fetch(
`https://api.wave.com/v1/transactions?date=${date}`,
{ headers: { 'Authorization': `Bearer ${process.env.WAVE_API_KEY}` } }
).then(r => r.json());
// جلب الطلبات Wave لليوم
const orders = await sql`
SELECT * FROM orders
WHERE payment_method = 'wave' AND DATE(paid_at) = ${date}
`;
// التطابق
const matches = [];
const unmatched = { wave: [], orders: [] };
for (const tx of waveTransactions) {
const order = orders.find(o => o.wave_session_id === tx.session_id);
if (order) matches.push({ tx, order });
else unmatched.wave.push(tx);
}
for (const order of orders) {
if (!matches.find(m => m.order.id === order.id)) {
unmatched.orders.push(order);
}
}
// تقرير email
await sendReport({
date,
total_matched: matches.length,
total_amount: matches.reduce((s, m) => s + m.tx.amount, 0),
unmatched
});
}
الخطوة 8 — Refunds
طلب ملغى = refund. Wave Refunds API.
async function refundOrder(orderId: string, reason: string) {
const order = await getOrder(orderId);
if (order.payment_method !== 'wave') return;
const res = await fetch('https://api.wave.com/v1/refunds', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.WAVE_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
session_id: order.wave_session_id,
amount: order.amount.toString(),
reason
})
});
if (res.ok) {
await sql`UPDATE orders SET status = 'refunded' WHERE id = ${orderId}`;
await sendSMS(order.customer_phone, 'تم استرداد المبلغ بالكامل في محفظة Wave الخاصة بك.');
}
}
الخطوة 9 — تخصيص Cash float للساعي
الساعي يبدأ يومه بـ 50,000 XOF نقد لإعادة الفرق. التطبيق يتتبع:
// schema courier
ALTER TABLE couriers ADD cash_float integer DEFAULT 50000;
// تحديث في كل توصيل cash
async function updateCashFloat(courierId: string, amountReceived: number, change: number) {
await sql`
UPDATE couriers
SET cash_float = cash_float + ${amountReceived} - ${change}
WHERE id = ${courierId}
`;
}
الخطوة 10 — Reporting و KPIs
Dashboard يعرض: % الدفعات Wave vs Cash، متوسط المبلغ، معدل الفشل، reconciliation status.
SELECT
payment_method,
COUNT(*) as count,
SUM(amount) as total,
AVG(amount) as avg
FROM orders
WHERE created_at > now() - interval '30 days'
GROUP BY payment_method;
-- نتيجة نموذجية:
-- wave | 1850 | 9,250,000 | 5,000
-- cash | 1230 | 6,150,000 | 5,000
-- om | 420 | 2,100,000 | 5,000
الأخطاء الشائعة
| الخطأ | السبب | الحل |
|---|---|---|
| Webhook 403 | signature خاطئة | raw body + HMAC SHA256 |
| Idempotency duplicate | idempotency-key مفقود | order-id كـ key |
| session expired | 30 دقيقة default | تجديد عند العودة من Wave |
| refund failed | session غير محدث | تحقق session_id في DB |
| Cash mismatch | الساعي ينسى تسجيل | تطبيق يفرض إدخال |
| QR code لا يعمل | payment link expired | regenerate كل 5 دقائق |
التكيف مع السياق
خمس توضيحات. Wave SN/CI/UG. Wave نشط في السنغال وكوت ديفوار وأوغندا. لـ مالي/بوركينا، Orange Money + Free Money. عمولة Wave 1%. أرخص بكثير من Stripe (2.9%) وVisa (3-4%). جذابة للهامش. Settlement فوري. Wave يحول إلى حسابك المصرفي يومياً (D+1). لا يجب الانتظار 7 أيام مثل Stripe. KYC صارم. Wave يتحقق من هوية التاجر والمعاملات. الالتزام بـ AML. Disputes نادرة. Wave يحل النزاعات بسرعة. أقل من 0.1% chargebacks.
دروس الإخوة
الأسئلة المتكررة
Wave vs Orange Money؟ Wave أرخص (1% vs 1.5%) وUX أبسط. OM مهيمن في كوت ديفوار. الاثنان معاً مثاليان.
Wave Cash-out؟ الساعي يستلم Wave، يحول إلى نقد عند Wave agent. عمولة 0.5% للسحب.
Wave Bulk Payment؟ ادفع 100 ساعٍ في وقت واحد عبر API Bulk. أقل تكلفة.
الأمن؟ Wave يستخدم E2E encryption + 2FA. لم يحدث اختراق رئيسي مذكور.
Sandbox للاختبار؟ Wave يقدم sandbox (api-sandbox.wave.com) لاختبار التكاملات قبل الإنتاج.
للاستزادة
- 🔝 المرجع: Stack لوجستية 2026
- Wave docs: docs.wave.com