| קובץ / רכיב | מצב עכשיו | מה ספרינט 1 עושה |
|---|---|---|
main.py | ✅ מלא | לא נוגעים |
models/models.py | ⚠️ חסר עמודה | F2: מוסיפות prayer_name ל-Donation |
schemas/schemas.py | ⚠️ PrayerResponse חלקי | F3: מרחיבות PrayerResponse ל-8 שדות |
alembic/versions/ | ❌ ריק | F2: יוצרות migration ראשוני — כל 6 טבלאות |
scripts/seed.py | ❌ לא קיים | F2: כותבות seed עם 5 קטגוריות + 5 תפילות |
services/prayer_service.py | ❌ NotImplementedError | F3: מממשות 4 פונקציות + helper _localize |
routers/prayers.py | ❌ מחזיר רשימות ריקות | F3: מחברות ל-service, 4 endpoints אמיתיים |
middleware/auth.py | 🔜 ספרינט 3 | לא נוגעים |
services/stripe_service.py | 🔜 ספרינט 2 | לא נוגעים |
| קובץ / רכיב | מצב עכשיו | מה ספרינט 1 עושה |
|---|---|---|
services/api.ts | ✅ מלא | לא נוגעים — קומפוננטת דוגמא |
store/authStore.ts | ✅ מלא | לא נוגעים — קומפוננטת דוגמא |
store/donationStore.ts | ✅ מלא | לא נוגעים — F6 קוראת לו |
constants/* | ✅ מלא | לא נוגעים |
types/* | ✅ מלא | לא נוגעים |
components/PrayerCard/ | ✅ מלא | לא נוגעים — קומפוננטת דוגמא |
app/_layout.tsx | ⚠️ חסרות 3 שורות | F4: מוסיפות signInAnon + StripeProvider + useAuth |
hooks/usePrayer.ts | ❌ stub ריק | F5: ממלאות usePrayers() | F6: ממלאות usePrayer(slug) |
app/(tabs)/index.tsx | ⚠️ TODO ברשימה | F5: FlatList עם PrayerCard + loading/error |
app/prayer/[slug].tsx | ⚠️ חסר not-found + polish | F6: מצב not-found, layout מסודר |
components/DonationWidget/ | ❌ TODO stub | F6: כפתורי tier עם highlight, כפתור "תרום" ריק |
components/DonationBottomSheet/ | 🔜 ספרינט 2 | לא נוגעים |
components/QuickButtons/ | 🔜 ספרינט 3 | stub בלבד — לא קורסת |
| משימה | Jira | גודל | תלויה ב | מי יכולה לבחור |
|---|---|---|---|---|
| Prereq — Dev Setup | ABD-5 | S | — | שתי המפתחות — חובה |
| 🚨 F2 — Migration + Seed | ABD-6 | M | Prereq | קחי ראשונה — gates הכל |
| F3 — Prayers API | ABD-7 | M | F2 | זמינה לאחר מיזוג F2 |
| F4 — App Foundation | ABD-8 | S | Prereq | עצמאית — תוכלי להתחיל מיד |
| F5 — Home Screen | ABD-9 | M | F3 + F4 | UI ניתן לפתח עם mock data לפני F3 |
| F6 — Prayer Page | ABD-10 | M | F5 | זמינה לאחר מיזוג F5 |
שתי המפתחות מקיימות את Prereq: Docker עולה, Backend רץ, Frontend עולה, שתיהן מאשרות GET /health → 200 ו-Expo web טוען. גם: שיחת 5 דקות על contract של PrayerResponse ↔ LocalizedPrayer.
מפתחת 1: מוסיפה prayer_name ל-Donation model, מריצה Alembic autogenerate, בודקת את קובץ ה-migration, מריצה upgrade head, כותבת seed.py ומריצה אותו. 5 תפילות ב-DB. פותחת PR → review → מיזוג.
מפתחת 2: עובדת על F4 — מוסיפה signInAnon() + StripeProvider + useAuth() ל-_layout.tsx (פחות מיום). אחרי זה מתחילה F5 עם mock data.
מפתחת 1: F3 — מרחיבה PrayerResponse, כותבת _localize() helper, מממשת 4 service functions, מחברת router. מאשרת: GET /api/prayers → 5 תפילות.
מפתחת 2: F5 — מוסיפה usePrayers() hook, מחברת FlatList ל-index.tsx עם loading/error states. ברגע שF3 נמזג — מסירה mock ועוברת ל-API אמיתי.
אחרי מיזוג F5: אחת מהמפתחות (או שתיהן יחד) עובדת על F6 — ממלאת usePrayer(slug), מסיימת את עמוד התפילה, מממשת DonationWidget עם כפתורי tier + highlight.
בסוף שבוע 2: flow שלם מהבית → תפילה → כפתורי תרומה עובד. כל ה-PRs עברו review + אישור של שרה.
| קובץ | שינוי | מי |
|---|---|---|
backend/app/models/models.py | שורה אחת: prayer_name = Column(String, nullable=True) | F2 |
backend/app/schemas/schemas.py | PrayerResponse מורחב + prayer_name ב-DonationCreate | F2+F3 |
backend/app/services/prayer_service.py | 4 functions + _localize helper (מ-NotImplementedError לקוד אמיתי) | F3 |
backend/app/routers/prayers.py | 4 endpoints מחוברים לservice (מ-stub לעובד) | F3 |
prayers-app/app/_layout.tsx | signInAnon() + StripeProvider + useAuth() (3 הוספות קטנות) | F4 |
prayers-app/hooks/usePrayer.ts | usePrayers() נוסף + usePrayer(slug) ממומש | F5+F6 |
prayers-app/app/(tabs)/index.tsx | FlatList + LoadingSpinner + error state | F5 |
prayers-app/app/prayer/[slug].tsx | not-found state + layout polish | F6 |
prayers-app/components/DonationWidget/DonationWidget.tsx | tier buttons עם selection highlight + כפתור תרום (ריק) | F6 |
prayers-app/i18n/he.json | מפתחות: error.loading, prayer.not_found, common.back, donation.donate | F5+F6 |
GET localhost:8000/health → 200localhost:8000/docs פתוח ועובדnpx expo start → w)cd backend
python -m venv .venv
.venv\Scripts\activate # Windows
# source .venv/bin/activate # Mac/Linux
pip install -r requirements.txt -r requirements-dev.txt
cp .env.example .env # ואז ממלאות:
# DATABASE_URL=postgresql://user:password@localhost:5432/prayers_db
# STRIPE_SECRET_KEY=sk_test_...
# FIREBASE_CREDENTIALS_PATH=./firebase-service-account.json
docker-compose up -d # PostgreSQL על port 5432
uvicorn app.main:app --reload # Server על port 8000
# בדיקה:
curl http://localhost:8000/health # → {"status": "ok"}
cd prayers-app npm install cp .env.example .env # ואז ממלאות: # EXPO_PUBLIC_API_URL=http://localhost:8000 # EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_... # EXPO_PUBLIC_FIREBASE_API_KEY=... (4 משתנים נוספים) npx expo start # → לוחצות w לפתוח web
serviceAccountKey.json מ-Firebase Console → שמות ב-backend/firebase-service-account.json → מוסיפות ל-.gitignore לפני כל commitsk_test_... הולך ל-backend/.env. pk_test_... הולך ל-prayers-app/.env. לא לבלבל.EXPO_PUBLIC_* הולכים רק ל-prayers-app/.env, לא ל-backend/.envdocker-compose upalembic/versions/ ריקprayer_name חסר מ-Donationseed.py לא קייםprayer_name)GET /api/prayers → [] (ריק, עד F3)seed.py אידמפוטנטי — בטוח לרוץ שובפותחות backend/app/models/models.py, מוצאות את class Donation (שורה ~65). אחרי donor_name:
# backend/app/models/models.py — בתוך class Donation: prayer_name = Column(String, nullable=True) # שם לתפילה — שונה מ-donor_name (שם לקבלה)
# backend/app/schemas/schemas.py — בתוך DonationCreate: prayer_name: str | None = None
cd backend
alembic revision --autogenerate -m "initial"
# → נוצר backend/alembic/versions/xxxx_initial.py
# ⚠️ פתחי את הקובץ שנוצר ובדקי:
# ✅ op.create_table('prayers', ...)
# ✅ op.create_table('donations', ...) — כולל prayer_name
# ✅ 6 טבלאות: prayers, users, donations, categories, quick_buttons, recurring_donations
# אם prayer_name חסר → מחקי את הקובץ, חזרי לשלב 1
alembic upgrade head # → No errors # ואז בדיקה ב-psql (או pgAdmin): \dt # → 6 טבלאות SELECT column_name FROM information_schema.columns WHERE table_name='donations'; # → חייב להופיע: prayer_name
יוצרות backend/scripts/seed.py (לא קיים). מריצות מתוך תיקיית backend:
cd backend python scripts/seed.py # → Categories ready: 5 # → Prayers seeded: 5 new # בדיקה: SELECT COUNT(*) FROM prayers; # → 5 SELECT COUNT(*) FROM categories; # → 5
prayer_name = Column(String, nullable=True) קיים ב-Donation modelalembic/versions/ ואוּמת ידניתalembic upgrade head רץ ללא שגיאותSELECT COUNT(*) FROM prayers → 5SELECT COUNT(*) FROM categories → 5alembic לא מוצא את המודלים — בדקי שה-venv פעיל ו-sys.path בـenv.py מכיל את app.
raise NotImplementedError[] ו-{} ריקיםGET /api/prayers?lang=he → 5 תפילותGET /api/prayers/health → תפילה בודדת + view_count++GET /api/prayers/search?q=רפואה → תוצאהGET /api/prayers/category/health → תפילה| Backend PrayerResponse | Frontend LocalizedPrayer | מיפוי |
|---|---|---|
id | id | str(prayer.id) |
slug | slug | ישיר |
title | title | מ-title_{lang} |
body | body | מ-body_{lang} |
seo_description | seoDescription | camelCase בצד frontend |
seo_keywords | seoKeywords | camelCase בצד frontend |
lang | lang | ישיר |
category_id | categoryId | camelCase בצד frontend |
# backend/app/schemas/schemas.py
class PrayerResponse(BaseModel):
id: str
slug: str
title: str | None = None
body: str | None = None
seo_description: str | None = None
seo_keywords: list[str] | None = None
lang: str = "he"
category_id: str | None = None
view_count: int = 0
model_config = ConfigDict(from_attributes=True)
# backend/app/services/prayer_service.py
def _localize(prayer: PrayerModel, lang: str) -> PrayerResponse:
"""Map Prayer ORM → PrayerResponse for a given lang, with Hebrew fallback."""
supported = ["he", "en", "fr", "ru", "es", "ar"]
if lang not in supported:
lang = "he"
title = getattr(prayer, f"title_{lang}", None) or prayer.title_he
body = getattr(prayer, f"body_{lang}", None) or prayer.body_he
seo_d = getattr(prayer, f"seo_description_{lang}", None) or prayer.seo_description_he
seo_kw = getattr(prayer, f"seo_keywords_{lang}", None) or prayer.seo_keywords_he or []
return PrayerResponse(
id=str(prayer.id),
slug=prayer.slug,
title=title, body=body,
seo_description=seo_d, seo_keywords=seo_kw,
lang=lang,
category_id=str(prayer.category_id) if prayer.category_id else None,
view_count=prayer.view_count,
)
# list_prayers
def list_prayers(db, lang="he"):
prayers = db.query(PrayerModel).filter(PrayerModel.is_active == True).all()
return [_localize(p, lang) for p in prayers]
# get_prayer_by_slug — מגדיל view_count!
def get_prayer_by_slug(db, slug, lang="he"):
prayer = db.query(PrayerModel).filter(
PrayerModel.slug == slug, PrayerModel.is_active == True
).first()
if not prayer:
raise HTTPException(status_code=404, detail="Prayer not found")
prayer.view_count += 1
db.commit()
return _localize(prayer, lang)
# list_by_category
def list_by_category(db, category, lang="he"):
prayers = (db.query(PrayerModel)
.join(Category, PrayerModel.category_id == Category.id)
.filter(Category.slug == category, PrayerModel.is_active == True).all())
return [_localize(p, lang) for p in prayers]
# search_prayers
def search_prayers(db, q, lang="he"):
title_col = getattr(PrayerModel, f"title_{lang}")
body_col = getattr(PrayerModel, f"body_{lang}")
prayers = (db.query(PrayerModel)
.filter(PrayerModel.is_active == True,
(title_col.ilike(f"%{q}%") | body_col.ilike(f"%{q}%"))).all())
return [_localize(p, lang) for p in prayers]
# backend/app/routers/prayers.py
# ⚠️ סדר חשוב: /search ו-/category/{cat} חייבים לפני /{slug}
# אחרת FastAPI יתפוס "search" כ-slug!
@router.get("/", response_model=list[PrayerResponse])
def list_prayers(lang: str = "he", db = Depends(get_db)):
return prayer_service.list_prayers(db, lang)
@router.get("/search", response_model=list[PrayerResponse])
def search(q: str, lang: str = "he", db = Depends(get_db)):
return prayer_service.search_prayers(db, q, lang)
@router.get("/category/{category}", response_model=list[PrayerResponse])
def by_category(category: str, lang: str = "he", db = Depends(get_db)):
return prayer_service.list_by_category(db, category, lang)
@router.get("/{slug}", response_model=PrayerResponse)
def get_prayer(slug: str, lang: str = "he", db = Depends(get_db)):
return prayer_service.get_prayer_by_slug(db, slug, lang)
GET /api/prayers?lang=he → 5 תפילות עם כל 8 שדותGET /api/prayers/health?lang=he → title: "תפילה לרפואה שלמה"/api/prayers/health → view_count עלה ב-DBGET /api/prayers/search?q=רפואה → מחזיר תפילת healthGET /api/prayers/category/health → מחזיר תפילת healthGET /api/prayers/nonexistent → 404 (לא 500)ruff check . עובר ללא שגיאותgetattr(PrayerModel, f"title_{lang}") (על המחלקה) מחזיר SQLAlchemy column — עובד ב-.ilike(). getattr(prayer_instance, f"title_{lang}") (על אובייקט) מחזיר string — עובד ב-_localize. לא לבלבל.
useAuth() לא נקרא1. הוסיפי import:
import { signInAnon } from '@/services/firebase';
import { StripeProvider } from '@stripe/stripe-react-native';
import { useAuth } from '@/hooks/useAuth';
2. הוסיפי signInAnon() ל-Promise.all:
Promise.all([ initI18n(), initializeStripe(), signInAnon(), // ← הוסיפי שורה זו ]).then(() => setReady(true));
3. עטפי עם StripeProvider + הוסיפי useAuth():
// בתוך function RootLayout():
useAuth(); // stub — Sprint 3 ימלא
// בתוך ה-return:
<StripeProvider publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? ''}>
<I18nextProvider i18n={i18n}>
<Stack screenOptions={{ headerShown: false }} />
</I18nextProvider>
</StripeProvider>
npm run type-check עוברnpm run lint עוברusePrayers(). ברגע שF3 נמזג — מסירות את ה-mock ועוברות ל-API אמיתי. מסירות mock לפני פתיחת PR.
usePrayers() לא קייםusePrayer(slug). אל תמחקי אותו — F6 תצטרך אותו!
// hooks/usePrayer.ts — הוסיפי אחרי ה-stub הקיים:
export function usePrayers(): {
prayers: LocalizedPrayer[];
isLoading: boolean;
error: string | null;
} {
const { lang } = useLanguageStore();
const [prayers, setPrayers] = useState<LocalizedPrayer[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
setIsLoading(true);
setError(null);
getPrayers()
.then((data) => setPrayers(data as LocalizedPrayer[]))
.catch((err: Error) => setError(err.message))
.finally(() => setIsLoading(false));
}, [lang]);
return { prayers, isLoading, error };
}
// app/(tabs)/index.tsx
export default function HomeScreen() {
const { t } = useTranslation();
const { prayers, isLoading, error } = usePrayers();
if (isLoading) return <LoadingSpinner />;
if (error) return <Text>{t('error.loading')}</Text>;
return (
<View style={styles.screen}>
<AppDownloadBanner /> {/* stub — Sprint 5 */}
<QuickButtons /> {/* stub — Sprint 3 */}
<FlatList
data={prayers}
keyExtractor={(p) => p.slug}
renderItem={({ item }) => <PrayerCard prayer={item} />}
/>
</View>
);
}
// i18n/he.json — הוסיפי אם לא קיים:
"error": {
"loading": "שגיאה בטעינה, נסה שוב"
}
/prayer/[slug]t('key')npm run lint + npm run type-check עובריםusePrayer(slug) תמיד מחזיר null<Text>TODO</Text>usePrayer.ts.
// hooks/usePrayer.ts — ממלאות את הגוף של usePrayer(slug):
export function usePrayer(slug: string) {
const { lang } = useLanguageStore();
const [prayer, setPrayer] = useState<LocalizedPrayer | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!slug) return;
setIsLoading(true);
getPrayer(slug)
.then((data) => setPrayer(data as LocalizedPrayer))
.catch((err: Error) => setError(err.message))
.finally(() => setIsLoading(false));
}, [slug, lang]);
return { prayer, isLoading, error };
}
export default function PrayerScreen() {
const { slug } = useLocalSearchParams<{ slug: string }>();
const { prayer, isLoading, error } = usePrayer(slug);
const { t } = useTranslation();
if (isLoading) return <LoadingSpinner />;
if (!prayer || error) return (
<View style={styles.center}>
<Text>{t('prayer.not_found')}</Text>
<Button label={t('common.back')} onPress={() => router.back()} />
</View>
);
return (
<View style={styles.screen}>
<ScrollView>
<Text style={styles.title}>{prayer.title}</Text>
<Text style={styles.body}>{prayer.body}</Text>
</ScrollView>
<View style={styles.bottomBar}>
<DonationWidget prayerId={prayer.id} />
</View>
</View>
);
}
constants/donations.ts ווודאו את שמות השדות ב-DonationTier. פתחו components/common/Button.tsx ווודאו את שם ה-prop (label לא text לא title).
export function DonationWidget({ prayerId }: { prayerId: string }) {
const { t } = useTranslation();
const { currency } = useLanguageStore();
const { selectedTier, selectTier } = useDonationStore();
const tiers = DONATION_TIERS[currency] ?? DONATION_TIERS.ILS;
return (
<View style={styles.container}>
<View style={styles.tiersRow}>
{tiers.map((tier) => {
const isSelected = selectedTier?.amount === tier.amount;
return (
<TouchableOpacity
key={tier.amount}
style={[styles.tierBtn, isSelected && styles.selected]}
onPress={() => selectTier(tier)}
>
<Text style={[styles.tierLabel, isSelected && styles.selectedLabel]}>
{tier.display}
</Text>
</TouchableOpacity>
);
})}
</View>
<Button
label={t('donation.donate')}
onPress={() => {
// TODO Sprint 2: setSheetOpen(true)
}}
/>
</View>
);
}
t('key')npm run lint + npm run type-check עובריםtypes/prayer.types.ts. 5 דקות שיחה חוסכת יום debugging.
Button, LoadingSpinner, PrayerCard — מה מלא, מה stub, מי משתמשת במה.
usePrayers() ועוברת ל-API אמיתי. מריצה end-to-end test. אם הכל עובד — commits, PR.
| קומפוננטה | ייבוא | מצב | מי משתמשת |
|---|---|---|---|
Button |
@/components/common |
✅ מלאה | F6 (not-found + DonationWidget) |
LoadingSpinner |
@/components/common |
⚠️ stub — עובד | F5 (בית) + F6 (עמוד תפילה) |
PrayerCard |
@/components/PrayerCard |
✅ מלאה — קומפוננטת דוגמא | F5 (בית) |
Input |
@/components/common |
stub — לא נדרש | — |
AppBottomSheet |
@/components/common |
stub — ספרינט 2 | — |
usePrayers() — F6 ממלאת usePrayer(slug)[₪72] = highlighted (נבחר) | שאר הכפתורים = רגילים
| פיצ'ר | מצב בסוף Sprint 1 | ספרינט |
|---|---|---|
| תשלום אמיתי (Stripe) | כפתור "תרום" ריק | Sprint 2 |
| DonationBottomSheet | stub ריק | Sprint 2 |
| Google/Apple Login | Anonymous בלבד | Sprint 3 |
| Quick Buttons (ממש) | stub מציג Text | Sprint 3 |
| LanguagePicker / RTL | עברית בלבד | Sprint 4 |
| SEO / SSR | Client-side only | Sprint 5 |
| EAS mobile build | Web בלבד | Sprint 5 |
docker-compose up -d && uvicorn app.main:app --reload → Backend עולהnpx expo start → w → האפליקציה נפתחת בדפדפןGET localhost:8000/api/prayers → JSON עם 5 תפילות עברית מלאותnpm run type-check ו-npm run lint עוברים ללא שגיאותruff check . עובר על Backend| ספרינט | נושא | עיקרי |
|---|---|---|
| Sprint 2 | תשלום | Stripe PaymentIntent, DonationBottomSheet, Webhook, Deploy ל-Render+Vercel |
| Sprint 3 | Auth + Quick | Firebase Auth middleware, Google/Apple Sign-In, כרטיס שמור, Quick Donate, QuickButtons |
| Sprint 4 | רב-לשוני | LanguagePicker, RTL/LTR, geo-IP currency, היסטוריית תרומות, Seed רב-שפתי |
| Sprint 5 | פרודקשן | SSR+SEO, EAS mobile build, App Store/Play, rate limiting, AppDownloadBanner, חיפוש |