ביטולים, החזרים וגבייה במנוי

שירות מנויים בישראל חייב לאפשר ביטול פשוט, לעצור את הוראת הקבע, להחזיר כסף לפי החוק, ולשלוח מסמך פרטי עסקה. כשהתשלום עובר בדף חיצוני של חברת סליקה, יש עוד מלכודות. ה-Skill מלמד את Claude לבנות את כל השכבה הזו, כולל החזר קרדיטים כשספק AI נכשל, ומגיע עם SQL ופונקציות לדוגמה. זהו Skill ל-Claude: תיקייה עם קובץ SKILL.md, שנטענת אוטומטית כשהמשימה מתאימה לתיאור שבראש הקובץ. בניגוד לפרומפט שמדביקים בכל פעם מחדש, Skill מותקן פעם אחת וממשיך לעבוד לבד.

עסק ישראלי · ביטול מנוי · החזרים · הוראת קבע · סליקה · צרכנות

מתי הוא נכנס לפעולה

---
name: israeli-cancellation-refunds
description: "Cancellation, refund and billing model for an Israeli subscription service: one-click cancel, prorated refunds, remote-sale cancellation fees, standing order versus one-time payment, terms acceptance before an external payment link, transaction document, receipt, and refunding usage credits when an AI provider fails. Use when building or auditing subscriptions, payments, pricing pages or refund policy."
---

# מודל ביטולים והחזרים, שירות מנויים בישראל

נבנה ונבדק בפרויקט אמיתי (<DOMAIN>) בספטמבר 2026. הקוד ב-`reference/` הוא הגרסה
שעלתה לאוויר ועברה בדיקות, אחרי ניקוי פרטים מזהים. **לא ייעוץ משפטי:** זה מה
שיושם ונבדק מול החוק כפי שהובן, ולפני השקה עם כסף אמיתי כדאי שעורך דין יעבור
על הנוסח.

## העיקרון שמעל הכול

**המסמכים נגזרים מהמימוש, ולא ההפך.** כל תקלה שנמצאה היתה פער בין מה שנכתב
למה שהמערכת עושה: "החיוב חד פעמי" כשהחודשי היה הוראת קבע, "נשלח תזכורת" כשאין
מנגנון, "כפתור ביטול בעמוד החשבון" לפני שנבנה. הבטחה בלשון הווה שאינה מתקיימת
היא הטעיה לפי חוק הגנת הצרכן, גם כשהיא נכתבה בתום לב.

לכן: **קודם בונים את המנגנון, אחר כך כותבים את המשפט, ואז בדיקה אוטומטית
שמשווה ביניהם.**

## המסגרת החוקית (חוק הגנת הצרכן, התשמ"א-1981)

| נושא | מה החוק דורש | איך מיישמים |
|---|---|---|
| **עסקה מתמשכת (§13ד)** | הצרכן רשאי לבטל בכל עת. החיוב נפסק תוך 3 ימי עסקים מההודעה. אסור לגבות על תקופה שאחרי הביטול | כפתור ביטול אמיתי, שנרשם בשרת עם חותמת זמן |
| **מכר מרחוק (§14ג)** | ביטול תוך 14 יום מההתקשרות או מקבלת המסמך, המאוחר מביניהם. דמי ביטול: **5% או 100 ש"ח, הנמוך**. החזר תוך 14 יום מההודעה | טבלה עם הסכומים המחושבים, לא נוסחה |
| **מסמך בכתב (§14ג(ב))** | מסירת מסמך עם פרטי העסקה, לא יאוחר מתחילת השירות | מייל אוטומטי ברגע ההפעלה. **זה לא הקבלה** |
| **הודעת ביטול** | בכל אמצעי שאפשר להוכיח בו את השליחה | מייל אישור מטריגר, שהוא ההוכחה של שני הצדדים |
| **הצגת מחיר** | המחיר המוצג לצרכן כולל מע"מ | "כולל מע״מ" ליד כל מחיר |
| **תקופת ביטול מוארכת** | לאדם עם מוגבלות, אזרח ותיק ועולה חדש, בעסקאות מסוימות (בעיקר כשהייתה שיחה) | ניסוח זהיר: "פנו אלינו ונטפל לפי הדין", לא הבטחה של מספר חודשים |

**קבלה היא חובה נפרדת מדיני המס.** מסמך פרטי העסקה חייב לצאת מיד. הקבלה יכולה
לצאת ממערכת חשבונאית חיצונית בתוך זמן סביר, ואז הזמן הזה הוא התחייבות שנכתבת
בכל מקום באותו מספר (בהפרויקט: `RECEIPT_HOURS = 48`).

## מודל העסק שצריך להחליט עליו קודם

שאלות לבעל העסק, **לפני שכותבים שורה**:

1. **אילו מסלולים הם הוראת קבע ואילו חד פעמיים?** זה נקבע בלוח הבקרה של הסולק,
   לא בקוד. בהפרויקט: חודשי = הוראת קבע עד 12 חודשים, שנתי = חד פעמי.
2. **ברירת המחדל בביטול:** גישה עד סוף התקופה ששולמה (מומלץ), והחזר יחסי למי
   שמבקש במקום זה, בשני המחזורים.
3. **איך מונפקת קבלה?** מהסולק, או ממערכת חיצונית, ותוך כמה זמן.
4. **מה קורה כשפעולה בתשלום נכשלת?** (ראו "החזר קרדיטים").

את התשובות כותבים כקבועים במקור אחד (`business.ts`, `payment-links.ts`),
והמסמכים והבדיקות קוראים מהם.

## המנגנונים, לפי סדר הבנייה

### 1. אישור תקנון לפני לינק תשלום חיצוני

ברגע שהגולש עובר לדף סליקה חיצוני, אין שום ראיה שהוא ראה את תנאי הביטול.
**הלינק נפתח רק אחרי שהאישור נרשם בשרת.**

- טבלה `terms_acceptance`: מי, מתי (`default now()` בשרת), גרסת התקנון, מסלול, מחזור.
- `accept_terms()` חסומה לאנונימי.
- **כשל ברישום עוצר את המעבר לתשלום.** אישור שלא נרשם אינו אישור.
- בחלון: סכום, סוג החיוב (הוראת קבע / חד פעמי), דמי הביטול המחושבים לעסקה,
  "סיכום ולא תחליף", קישורים למסמכים, ותיבת סימון לא מסומנת מראש.
- `TERMS_VERSION` עולה בכל שינוי מהותי בתנאים. **לפני ששינית תקנון, בדוק כמה
  אישרו את הגרסה הנוכחית.** אם אפס, אפשר לתקן בלי להעלות גרסה.

קוד: `reference/components/TermsGate.tsx`, `reference/sql/02_terms_acceptance.sql`.

### 2. תשלום ← גישה, כשהלינק אינו נושא מזהה משתמש

דפי תשלום קבועים (GROW ודומיו) לא יודעים מי שילם. בלי מנגנון, אדם משלם ולא
מקבל גישה, וזה המצב שממנו נולדות תלונות.

- טבלת `payment_claims` + מייל מיידי לבעל העסק + אישור בלחיצה במסך הניהול.
- כתובת הצלחה בסולק: `/account?paid=<plan>&cycle=<cycle>` פותחת בקשה אוטומטית.
- כפתור "שילמתי ולא נפתחה גישה" למי שסגר את הלשונית.
- **אישור מאריך מהתאריך המאוחר מבין "עכשיו" ו"תוקף נוכחי"**, כדי שחידוש לא יאבד ימים.

קוד: `reference/sql/01_payment_claims_and_notify.sql`, `reference/components/claims.ts`.

### 3. מסמך פרטי עסקה, מיד

טריגר על אישור התשלום → פונקציית ענן → מייל עם: שם החברה, ח.פ., כתובת, השירות,
**סכום החיוב הבודד**, אופן החיוב, תחילת השירות, תוקף, זכות הביטול, ו"קבלה תגיע
תוך X שעות".

**בהוראת קבע הקבלה היא על כל חיוב בנפרד, בסכום החיוב עצמו.** מסמך שמציג סכום
שנתי מול חיוב חודשי הוא הטעיה.

קוד: `reference/functions/notify/index.ts` (`purchaseDocHtml`),
`reference/sql/03_cancel_and_transaction_doc.sql` (`notify_purchase`).

### 4. כפתור ביטול

```sql
-- הנקודה החשובה: pro_until לא מאופס. הגישה נמשכת עד סוף מה ששולם.
update subscriptions set status = 'canceled', canceled_at = now(),
  refund_status = case when p_want_refund then 'requested' else refund_status end
where id = v_id;
```

- `cancel_my_subscription(p_reason, p_want_refund)` לפי `auth.uid()`, לא לפי פרמטר.
- למסלול שהוענק ידנית ואין לו שורת מנוי, **יוצרים שורה מבוטלת**, אחרת אין רישום.
- תיבת "אני מעדיף החזר יחסי" בחלון הביטול, כי זו זכות ולא טובה.
- **מייל אישור מטריגר ולא מהדפדפן**, עם התאריך שעד אליו הגישה נמשכת.
- **בהוראת קבע: התראה אדומה לבעל העסק לעצור אותה אצל הסולק.** הקוד לא יכול.
  מייל הביטול ללקוח אומר "אנחנו עוצרים אותה, ואם תראו חיוב נוסף נחזיר אותו".

קוד: `reference/sql/03_cancel_and_transaction_doc.sql`.

### 5. החזר קרדיטים כשספק נכשל

**הבאג שנמצא:** `charge()` רץ לפני הקריאה למודל, ושום מסלול כישלון לא החזיר.
לקוח משלם הפסיד קרדיטים גם כשגוגל היתה עמוסה.

המדיניות שנבחרה (החלטה עסקית, לכתוב בתקנון):
- **כישלון אצלנו או אצל הספק → מוחזר.**
- **חסימה של מסנני הבטיחות → נספרת**, כי התוכן של המשתמש גרם לה, והחזר עליה
  מאפשר לשלוח תוכן אסור בלולאה בחינם בזמן שכל ניסיון עולה קריאה.

יישום:
- `spend_credits` מחזירה `from_grant` ו-`from_topup`. **ההחזר חייב לחזור לאותו
  מקום.** החזר שמעביר קרדיטים שנקנו למענק החודשי לוקח מהלקוח כסף בשם ההחזר.
- החיוב נשמר במשתנה **מחוץ ל-try**, כדי שגם חריגה אחרי הניכוי תחזיר.
- עבודה אסינכרונית (תמלול): הקבלה נשמרת **בשרת** לפי מזהה העבודה ונשלפת פעם
  אחת (`delete ... returning`). **לעולם לא מחזירים קבלה ללקוח**, קבלה שהלקוח
  שולח בחזרה היא מכונה להדפסת קרדיטים.
- **כל פונקציות ההחזר `service_role` בלבד.**

קוד: `reference/sql/04_refund_on_provider_failure.sql`.

## עמוד הביטול וההחזרים

סדר הסעיפים ב-`reference/pages/Refunds.tsx`: השורה התחתונה, איך מבטלים (שלוש
דרכים, הראשונה כפתור), מה קורה לגישה, החזר יחסי, מכר מרחוק עם טבלת סכומים
מחושבת מהמחירון, חידוש (נגזר מסוג החיוב), מסמכי עסקה וקבלה, מה השירות מספק
ומה לא, יצירת קשר עם פרטי החברה.

**דמי הביטול מחושבים ולא מוקלדים:**
```ts
export const cancellationFee = (price: number) =>
  Math.min((Number.isFinite(price) && price > 0 ? price : 0) * 0.05, 100)
```

## בדיקות חובה (להוסיף לחבילת הבדיקות של הפרויקט)

- **סוג החיוב במסמכים נגזר מתצורת התשלום:** אם יש קישור `recurring`, התקנון,
  עמוד ההחזרים והמחירון אומרים "הוראת קבע". אף אחד מהם לא אומר "חד פעמי לכל תקופה".
- **אין הבטחה בלי מנגנון:** אם אין טריגר תזכורת, אף מסמך לא אומר "נשלח תזכורת".
- הלינק לתשלום לא נפתח ישירות בלחיצה, רק מתוך `onApproved`.
- כשל ברישום האישור עוצר את המעבר.
- הביטול לא נוגע ב-`pro_until`.
- פונקציות ההחזר חסומות ל-`anon` ול-`authenticated`.
- `RECEIPT_HOURS` בקוד הלקוח זהה לזה שבפונקציית הענן (עותקים נפרדים, השוואה).

**ואחרי כתיבת בדיקה: מוטציה.** מחזירים זמנית את הנוסח השגוי ומוודאים שהבדיקה
נופלת. בדיקה שכתבה את הטעות כמצופה ("החיוב הוא חד פעמי לכל תקופה") רק מגינה
עליה. ובמוטציה עצמה לוודא שהקובץ **באמת השתנה**, כי עוגן שלא נמצא נראה בדיוק
כמו בדיקה שלא תפסה.

## אימות מול המסד, בלי לשלוח מייל ובלי לשנות נתונים

בלוק `do` שמתחזה למשתמש אמיתי, מריץ את כל המסלול ונגמר ב-`raise exception`
שנושא את התוצאות. החריגה מגלגלת הכול, כולל בקשות `pg_net` בתור, ולכן אף מייל
לא יוצא:

```sql
do $
declare r text[] := array[]::text[]; res jsonb;
begin
  perform set_config('request.jwt.claims',
    json_build_object('sub', '<USER_ID>', 'role', 'authenticated')::text, true);
  res := public.cancel_my_subscription('בדיקה', true);
  r := array_append(r, res::text);
  raise exception 'TESTRESULT %', array_to_string(r, ' || ');
end $;
```

**`perform` על פונקציה שמחזירה אפס שורות לא זורק.** "לא נזרקה שגיאה" אינו
"הוחזרו נתונים". לספור שורות במפורש (`select count(*) into n from f()`).

## לפני השקה עם כסף, רשימה לבעל העסק

1. כתובת הצלחה מוגדרת בסולק, או שכל מכירה = אישור ידני במסך הניהול.
2. נוהל קבלה בזמן שהובטח.
3. נוהל עצירת הוראת קבע אצל הסולק בכל ביטול.
4. אכיפת מסלולים דלוקה, אחרת מה שנמכר ניתן בחינם לכולם.
5. המסמכים נקראו פעם אחת מול המחירון בפועל.

איך מתקינים

  1. צרו תיקייה בשם israeli-cancellation-refunds בתוך ‎.claude/skills/‎ בפרויקט, או בתיקיית הבית לשימוש בכל הפרויקטים.
  2. שמרו בתוכה את הקובץ בשם SKILL.md.
  3. זהו. אין צורך להפעיל כלום, והוא ייטען לבד כשהמשימה תתאים לתיאור.