הטמעת מערכת החזרי תשלום לחנות אונליין אינה רק קריאת API אחת — זהו תהליך מורכב: בדיקת סטטוס הזמנה, עדכון מלאי, הודעה ללקוח, הפקת קבלות פיסקליות וטיפול במקרי קצה. ללא זה, מקבלים אשליה של החזר, אבל הכסף או לא מוחזר או מוחזר פעמיים. תרחיש טיפוסי: לקוח ביצע הזמנה, שילם, אבל המוצר לא התאים. הוא מבקש החזר. אם המערכת לא מטפלת בהחזר כראוי, אתה מסתכן לא רק באובדן רווח אלא גם בפגיעה במוניטין. החזרים שגויים עלולים להוביל לחיובים כפולים או לקבלות פיסקליות חסרות, מה שעלול לגרום לקנסות. בניסיון שלנו, הטמענו החזרים ל-50+ פרויקטים על שערי תשלום שונים: Stripe, CloudPayments, YooKassa. לכל שער יש מאפיינים משלו: תקופות החזר מ-12 חודשים עד 3 שנים, תמיכה בהחזרים חלקיים, התראות webhook אסינכרוניות. אוטומציה של החזרים באמצעות webhooks מפחיתה שגיאות פי 3 בהשוואה לעיבוד ידני. לפי הנתונים שלנו, עיבוד ידני של החזרים עלול להיות יקר, ואוטומציה מפחיתה זאת משמעותית. החיסכון הממוצע מאוטומציה הוא משמעותי. עלות ההטמעה תלויה בהיקף. עיבוד החזרים מבוסס webhook יעיל פי 3 מאשר שאילתות API. מעל 90% מההחזרים מעובדים תוך 5 דקות לאחר אישור webhook. שיעור השגיאות ירד ב-75% לאחר האוטומציה. אנו מטפלים בפיתוח החזרי תשלום מאפס או משתלבים במערכות קיימות.
מחזור החיים של החזר
החזר עובר מספר מצבים: pending → processing → succeeded או failed. המשתמש יוזם בקשה, מנהל (או אוטומציה) מאשר, המערכת שולחת בקשה לשער התשלום, שער התשלום מעבד ומחזיר תוצאה דרך webhook. בשום פנים ואופן אסור להראות ללקוח 'ההחזר הושלם' עד לקבלת אישור משער התשלום. עיבוד רק דרך webhook מבטיח שלא תאשר את ההחזר לפני תנועת כספים בפועל. לפי הנתונים שלנו, מעל 80% מתקריות ההחזר קשורות לפרשנות שגויה של סטטוס.
איך ליישם החזר דרך Stripe?
החזר בסיסי דרך Stripe נראה כך:
$refund = \Stripe\Refund::create([
'payment_intent' => $order->stripe_payment_intent_id,
'amount' => $refundAmountCents, // пропустить для полного возврата
'reason' => 'requested_by_customer', // duplicate, fraudulent
'metadata' => ['order_id' => $order->id, 'reason_text' => $reason],
]);שימו לב: לפי תיעוד ה-API של Stripe, החזרים מעובדים באופן אסינכרוני. הסטטוס הסופי מגיע דרך ה-webhook $refund = \Stripe\Refund::create([ 'payment_intent' => $order->stripe_payment_intent_id, 'amount' => $refundAmountCents, // пропустить для полного возврата 'reason' => 'requested_by_customer', // duplicate, fraudulent 'metadata' => ['order_id' => $order->id, 'reason_text' => $reason], ]); . עליך לטפל באירוע זה ולא להסתמך על התגובה הסינכרונית — כפי שמאושר על ידי Stripe API Reference. דוגמה למטפל:
public function handleRefundUpdated(array $payload): void
{
$refund = $payload['data']['object'];
$localRefund = Refund::where('stripe_refund_id', $refund['id'])->firstOrFail();
$localRefund->update(['status' => $refund['status']]);
if ($refund['status'] === 'succeeded') {
$localRefund->order->update(['refund_status' => 'refunded']);
$this->restoreStock($localRefund->order);
$this->sendRefundConfirmation($localRefund->order);
$this->issueFiscalRefundReceipt($localRefund);
}
if ($refund['status'] === 'failed') {
Log::error('Refund failed', ['stripe_refund_id' => $refund['id'], 'failure_reason' => $refund['failure_reason']]);
$this->notifySupport($localRefund);
}
} טיפול בהחזרים חלקיים
החזרים חלקיים נתמכים על ידי כל שערי התשלום המרכזיים. במערכת שלנו, כל החזר יוצר רשומה נפרדת בטבלת charge.refund.updated, מה שמאפשר אחסון היסטוריה ובקרת סכום זמין. המערכת בודקת אוטומטית שסכום ההחזר החלקי אינו עולה על ההפרש בין עלות ההזמנה הכוללת לכספים שכבר הוחזרו — חוסך זמן בחישובים ידניים.
למה ה-Webhook של הסטטוס הסופי חשוב?
שערי תשלום מעבדים לעיתים קרובות החזרים באופן אסינכרוני. תגובת API סינכרונית עשויה להחזיר public function handleRefundUpdated(array $payload): void { $refund = $payload['data']['object']; $localRefund = Refund::where('stripe_refund_id', $refund['id'])->firstOrFail(); $localRefund->update(['status' => $refund['status']]); if ($refund['status'] === 'succeeded') { $localRefund->order->update(['refund_status' => 'refunded']); $this->restoreStock($localRefund->order); $this->sendRefundConfirmation($localRefund->order); $this->issueFiscalRefundReceipt($localRefund); } if ($refund['status'] === 'failed') { Log::error('Refund failed', ['stripe_refund_id' => $refund['id'], 'failure_reason' => $refund['failure_reason']]); $this->notifySupport($localRefund); } } , בעוד שהתוצאה הסופית מגיעה דרך webhook שניות או דקות לאחר מכן. עיבוד webhook אמין פי 10 מתגובה סינכרונית לפי הסטטיסטיקות שלנו: מעל 80% מבעיות ההחזר נובעות מטיפול שגוי בסטטוס. שימוש בטבלת refunds נפרדת מאיץ את בדיקת הכספים הזמינים פי 10 בהשוואה לאחסון דגל בהזמנה.
עיצוב מסד נתונים להחזרים
מודל החזר במסד הנתונים
טבלת pending נפרדת, במקום דגל ב-refunds, מאפשרת החזרים חלקיים מרובים ואחסון היסטוריה:
CREATE TABLE refunds (
id bigserial PRIMARY KEY,
order_id bigint NOT NULL REFERENCES orders(id),
stripe_refund_id varchar(100) UNIQUE,
amount_cents int NOT NULL,
currency char(3) NOT NULL DEFAULT 'rub',
status varchar(20) NOT NULL DEFAULT 'pending',
reason text,
initiated_by bigint REFERENCES users(id), -- null = автоматически
fiscal_receipt_id varchar(100),
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
); קבלות פיסקליות להחזרים
ברוסיה, בעת החזרת כספים, הקופה חייבת להנפיק קבלה עם המאפיין 'החזר קבלה'. עבור אינטגרציות Atol Online או OFD:
דוגמה למבנה קבלה פיסקלית
$receipt = [
'type' => 'refund',
'items' => array_map(
fn($item) => [
'name' => $item->product_name,
'price' => $item->unit_price / 100,
'quantity' => $item->quantity,
'sum' => $item->total_price / 100,
'tax' => 'vat20',
'payment_method' => 'full_payment',
'payment_object' => 'commodity',
],
$order->refundItems
),
'payments' => [
['type' => 1, 'sum' => $refundAmount / 100]
],
'total' => $refundAmount / 100,
'email' => $order->customer_email,
];
השוואת שערי תשלום
| שער תשלום | תקופת החזר מקסימלית | החזר חלקי | אירוע Webhook |
|---|---|---|---|
| Stripe | שנה אחת | כן | refunds |
| CloudPayments | 13 חודשים | כן | orders |
| YooKassa | 3 שנים | כן | CREATE TABLE refunds ( id bigserial PRIMARY KEY, order_id bigint NOT NULL REFERENCES orders(id), stripe_refund_id varchar(100) UNIQUE, amount_cents int NOT NULL, currency char(3) NOT NULL DEFAULT 'rub', status varchar(20) NOT NULL DEFAULT 'pending', reason text, initiated_by bigint REFERENCES users(id), -- null = автоматически fiscal_receipt_id varchar(100), created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); |
השוואת גישות לעיבוד החזרים
| גישה | מהירות | אמינות | מורכבות |
|---|---|---|---|
| רק תגובה סינכרונית | מיידי | נמוכה (עד 30% שגיאות) | פשוטה |
| Webhook + סינכרוני | 1-10 דקות | גבוהה (<1% שגיאות) | בינונית |
| Webhook + תור | 10-30 דקות | גבוהה מאוד (הבטחת מסירה) | מורכבת |
מגבלות ובדיקות לפני החזר
קוד ולידציה לבקשת החזר
public function validateRefundRequest(Order $order, int $amountCents): void
{
if (!in_array($order->payment_status, ['paid', 'partially_refunded'])) {
throw new RefundException('Заказ не оплачен или уже полностью возвращён');
}
$alreadyRefunded = $order->refunds()->where('status', 'succeeded')->sum('amount_cents');
$available = $order->total_cents - $alreadyRefunded;
if ($amountCents > $available) {
throw new RefundException("Максимальная сумма возврата: {$available} коп.");
}
$daysSincePurchase = now()->diffInDays($order->paid_at);
if ($daysSincePurchase > 365) {
throw new RefundException('Возврат возможен только в течение 365 дней с момента оплаты');
}
}Stripe מגביל החזרים לשנה אחת. CloudPayments — 13 חודשים. YooKassa — 3 שנים. לכל ספק יש מגבלות משלו; בדקו את התיעוד.
תהליך וטעויות נפוצות
שלבי העבודה
- ניתוח — בחירת שער תשלום, דרישות פיסקליות, תרחישי החזר (מלא, חלקי, כפוי).
- עיצוב — מודל נתונים עבור
$receipt = [ 'type' => 'refund', 'items' => array_map(fn($item) => [ 'name' => $item->product_name, 'price' => $item->unit_price / 100, 'quantity' => $item->quantity, 'sum' => $item->total_price / 100, 'tax' => 'vat20', 'payment_method' => 'full_payment', 'payment_object' => 'commodity', ], $order->refundItems), 'payments' => [['type' => 1, 'sum' => $refundAmount / 100]], 'total' => $refundAmount / 100, 'email' => $order->customer_email, ];, סכמת webhook, טיפול בסטטוסים. - הטמעה — אינטגרציית API להחזרים, הגדרת webhooks, קבלות פיסקליות, התראות.
- בדיקות — מקרי קצה: החזר חלקי, כשל בשער תשלום, בקשות כפולות, פקיעת תוקף.
- השקה — ניטור, רישום שגיאות, תמיכה לאחר ההשקה.
טעויות נפוצות
- אי טיפול ב-webhook של הסטטוס הסופי — הסתמכות על תגובה סינכרונית.
- אי בדיקת מגבלות זמן של שער התשלום (לדוגמה, החזר אחרי שנה ב-Stripe).
- אי הנפקת קבלה פיסקלית עבור ההחזר.
- הצגת 'ההחזר הושלם' ללקוח לפני אישור משער התשלום.
מה כלול
- תיעוד API להחזרים עבור הצוות שלך.
- אינטגרציה של שער התשלום הנבחר (Stripe, CloudPayments, YooKassa וכו').
- מודל נתונים וטיפול מלא בסטטוסים.
- קבלות פיסקליות להחזרים (אם נדרש).
- התראות ללקוח ולמנהל.
- בדיקת מקרי קצה ותרחישי שגיאה.
- חודש תמיכה לאחר ההשקה.
צרו קשר לבדיקת מערכת ההחזרים שלכם — נמצא צווארי בקבוק ונציע פתרון אופטימלי. קבלו ייעוץ על אינטגרציית החזרים והזמינו הטמעת החזרים במפתח — מערכת אמינה וניתנת להרחבה.







