Commit 733ed4d3 authored by Mahmoud Aglan's avatar Mahmoud Aglan

Add essential product duplicate warning in POS + update mobile app plan

POS Terminal now checks if an essential product was already purchased by
the selected participant this year. Shows amber warning with the purchase
date — allows the sale to proceed, just warns the admin.

Also expanded mobile app plan with: explore academy (news, gallery),
shop for essential products from app, event registration, and the
corresponding API endpoints needed on the system side.
Co-Authored-By: 's avatarClaude Opus 4.6 <noreply@anthropic.com>
parent b8cde029
# Mobile App Plan — Client App (Guardians & Participants)
## Vision
A visually stunning, data-driven mobile app for guardians and participants. Push notifications that work even when app is closed. Not a business management app — it's the client-facing experience.
## Target Users
- **Guardians (parents)** — see their children's progress, pay invoices, get notified
- **Participants (players)** — see their schedule, attendance, achievements
---
## Features (Client-Facing)
### 1. Authentication & Onboarding
- Phone number + OTP login (Egypt: +20)
- Link guardian to existing account (by phone or national ID)
- Participant self-login (age 14+) via phone
- Biometric (fingerprint/face) for quick re-entry
- Academy branding on login screen (logo, colors from website_settings)
### 2. Dashboard (Home Screen)
- Upcoming sessions (today/this week) with countdown timers
- Outstanding balance / next payment due
- Recent attendance (last 5 sessions — present/absent indicators)
- Academy announcements (from notification_templates)
- Quick actions: pay now, report absence, contact academy
### 3. Schedule & Calendar
- Weekly/monthly view of all sessions
- Color-coded by group/program
- Session details: time, facility, trainer, group
- Cancelled/rescheduled sessions highlighted
- Add to phone calendar (ICS export)
### 4. Attendance & Progress
- Attendance history (calendar heatmap view)
- Attendance rate percentage (visual ring)
- Late arrivals tracked
- Streak tracking (consecutive present days)
- Monthly attendance summary
### 5. Financial
- Outstanding invoices list
- Payment history
- Pay now (online payment gateway integration)
- Installment plan view (paid/upcoming/overdue)
- Download receipt (PDF)
- Wallet balance (if applicable)
### 6. Profile & Children
- Guardian sees all linked children
- Switch between children
- Each child: photo, group, program, trainer, enrollment date
- Medical certificate expiry warning
- Documents (uploaded/pending)
### 7. Notifications (THE CORE FEATURE)
- Push notifications via FCM (Firebase Cloud Messaging)
- Works when app is closed (background/terminated)
- Types:
- Session reminder (30 min before)
- Attendance marked (your child was marked present/absent)
- Invoice created / payment due
- Payment confirmed
- Session cancelled/rescheduled
- Announcement from academy
- Medical certificate expiring
- Installment due tomorrow
- Enrollment confirmed
- Group transfer notification
- In-app notification center (history)
- Read/unread state
- Notification preferences (per type toggle)
### 8. Communication
- Direct message to academy (simple chat or form)
- Report absence in advance
- Request transfer/freeze
- Feedback after session (optional, academy-configurable)
### 9. Explore Academy
- Academy about/info (pulled from website_settings)
- News feed (from website news module)
- Photo/video gallery
- Branches & locations (with map)
- Working hours
- Social links
- Contact info
- Programs offered (browse available programs)
### 10. Events
- Upcoming events list (from website events module)
- Event details + registration (join from app)
- Event gallery (photos/videos after event)
- Push notification when new event is published
### 11. Shop — Essential Products
- Browse essential products available for purchase
- See product details, photos, price
- Buy from app (online payment)
- Installment plans visible + selectable
- Order status tracking (pending delivery → delivered)
- Purchase history
- System warns if product already purchased this year (same logic as POS)
### 12. Kit / Uniform Ordering
- If academy sells kits with specific sizes
- Size selection
- Add to cart + pay or request from academy
---
## System-Side Preparations (Laravel Backend)
### A. API Layer — NEW: `routes/api.php`
We need a full REST API. Currently the system is Livewire-only (server-rendered). The mobile app needs JSON endpoints.
**Auth endpoints:**
- `POST /api/auth/otp/request` — send OTP to phone
- `POST /api/auth/otp/verify` — verify OTP, return Sanctum token
- `POST /api/auth/logout` — revoke token
- `GET /api/auth/me` — current user + linked participants
**Data endpoints:**
- `GET /api/participants/{uuid}` — profile
- `GET /api/participants/{uuid}/schedule` — upcoming sessions
- `GET /api/participants/{uuid}/attendance` — history + stats
- `GET /api/participants/{uuid}/invoices` — outstanding + paid
- `GET /api/participants/{uuid}/enrollments` — active programs
- `GET /api/participants/{uuid}/documents` — uploaded docs
- `GET /api/guardian/children` — all linked participants
**Action endpoints:**
- `POST /api/payments/initiate` — start online payment
- `POST /api/payments/callback` — payment gateway webhook
- `POST /api/absences/report` — report planned absence
- `POST /api/messages/send` — contact academy
- `POST /api/notifications/preferences` — update notification prefs
- `PATCH /api/notifications/{id}/read` — mark as read
- `GET /api/notifications` — paginated notification history
**Shop endpoints:**
- `GET /api/participants/{uuid}/products` — available essential products (with already-purchased flag)
- `POST /api/orders/create` — purchase product from app
- `GET /api/participants/{uuid}/orders` — order history
- `GET /api/orders/{uuid}` — order status/details
**Academy explore endpoints:**
- `GET /api/academies/{slug}/news` — news feed
- `GET /api/academies/{slug}/gallery` — photo/video gallery
- `GET /api/academies/{slug}/programs` — available programs
**Public endpoints (no auth):**
- `GET /api/academies/{slug}/info` — academy branding for login screen
- `GET /api/academies/{slug}/events` — public events
- `POST /api/events/{uuid}/register` — join event (auth required)
### B. Push Notification Infrastructure
**Database changes:**
```
// New migration: device_tokens table
- id
- user_id (FK)
- device_token (string, unique) — FCM token
- platform (enum: ios, android)
- device_name (nullable)
- is_active (bool, default true)
- last_used_at (timestamp)
- created_at, updated_at
// New migration: notification_logs additions
- Add column: push_sent_at (nullable timestamp)
- Add column: push_status (enum: pending, sent, failed, delivered)
- Add column: fcm_message_id (nullable string)
```
**New service: `PushNotificationService`**
- Register device token
- Send push via FCM HTTP v1 API
- Handle token refresh (old token → new token)
- Handle unregistered tokens (remove from DB)
- Batch sending (multiple recipients)
- Priority: high (payment/attendance) vs normal (announcements)
**FCM Integration:**
- Firebase project setup (one per academy OR one shared project)
- Server key stored in `system_settings` or `.env`
- Package: `kreait/firebase-php` (official Firebase Admin SDK for PHP)
**Event → Push mapping (extend existing listeners):**
```
AttendanceMarked → push to guardian ("تم تسجيل حضور [اسم] اليوم")
InvoiceCreated → push to guardian ("فاتورة جديدة بقيمة X ج.م")
PaymentConfirmed → push to payer ("تم تأكيد الدفع")
SessionCancelled → push to group participants' guardians
SessionReminder → scheduled job, 30 min before each session
InstallmentDue → scheduled job, 1 day before due_date
MedicalCertExpiring → scheduled job, 7 days before expiry
Announcement → push to all active participants' guardians
```
### C. Authentication — Sanctum API Tokens
**What exists:** Session-based auth (web only)
**What's needed:** Token-based auth for mobile
```php
// config/sanctum.php — already installed with Laravel
// Add to User model:
use Laravel\Sanctum\HasApiTokens;
// Token abilities for mobile:
'mobile:read' read own data
'mobile:pay' initiate payments
'mobile:notify' manage notification preferences
```
**OTP Flow:**
1. User enters phone → system finds User by phone
2. Generate 6-digit OTP, store in cache (5 min TTL)
3. Send via SMS gateway (existing SMS interface)
4. User enters OTP → verify → issue Sanctum token
5. Token stored on device, sent as `Bearer` header
### D. Online Payment Gateway
**Options for Egypt:**
- Paymob (most common, supports cards + wallets)
- Fawry (kiosk + online)
- Kashier
**Integration points:**
- `POST /api/payments/initiate` → create Paymob intention → return payment URL/iframe key
- Webhook: `POST /api/payments/callback` → verify HMAC → confirm payment → update invoice
- Mobile app opens payment URL in WebView or uses Paymob SDK
### E. Scheduled Jobs (new or modified)
```
// New jobs for mobile push
schedule:run
├── SendSessionReminders — every minute, find sessions starting in 30 min
├── SendInstallmentReminders — daily 9am, find installments due tomorrow
├── SendMedicalCertWarnings — daily, find certs expiring in 7 days
└── CleanExpiredDeviceTokens — weekly, remove tokens unused for 90 days
```
### F. API Resource Classes (response formatting)
```
app/Http/Resources/
├── ParticipantResource.php
├── SessionResource.php
├── AttendanceResource.php
├── InvoiceResource.php
├── PaymentResource.php
├── EnrollmentResource.php
├── NotificationResource.php
├── AcademyInfoResource.php
└── EventResource.php
```
### G. Rate Limiting & Security
- Rate limit OTP: 3 attempts per phone per 10 min
- Rate limit API: 60 requests/min per token
- Token expiry: 30 days (refresh on each use)
- Device token validation (only accept valid FCM tokens)
- API versioning: `/api/v1/...`
---
## Build Order (System Side)
### Phase 1: API Foundation
1. Install/configure Sanctum for API tokens
2. Create `routes/api.php` with versioned prefix
3. OTP auth flow (request → verify → token)
4. Guardian/participant read endpoints
5. API Resources for response formatting
### Phase 2: Push Notifications
6. `device_tokens` migration
7. `PushNotificationService` (FCM HTTP v1)
8. Device token registration endpoint
9. Extend existing event listeners to trigger push
10. Session reminder scheduled job
### Phase 3: Financial API
11. Invoice list/detail endpoints
12. Payment gateway integration (Paymob)
13. Payment initiation + webhook
14. Installment tracking endpoints
### Phase 4: Actions & Communication
15. Report absence endpoint
16. Notification preferences endpoint
17. Message/contact academy endpoint
18. Notification history endpoint
### Phase 5: Polish
19. Rate limiting middleware
20. API documentation (auto-generated)
21. Error response standardization (Arabic messages)
22. Push notification preferences (per-type toggle)
---
## What The Flutter App Needs From Us
| Feature | Endpoint | Push Event |
|---------|----------|------------|
| Login | `POST /api/auth/otp/*` | — |
| Home dashboard | `GET /api/participants/{id}/summary` | — |
| Schedule | `GET /api/participants/{id}/schedule` | session_cancelled, session_reminder |
| Attendance | `GET /api/participants/{id}/attendance` | attendance_marked |
| Invoices | `GET /api/participants/{id}/invoices` | invoice_created |
| Pay | `POST /api/payments/initiate` | payment_confirmed |
| Notifications | `GET /api/notifications` | (all types) |
| Profile | `GET /api/participants/{id}` | — |
| Events | `GET /api/academies/{slug}/events` | event_announcement |
---
## Decisions Needed
1. **Payment gateway** — Paymob? Fawry? Both?
2. **FCM project** — One shared Firebase project or per-academy?
3. **SMS provider for OTP** — Use existing SMS gateway or add Twilio/Vonage?
4. **App branding** — One generic app branded per academy at runtime? Or separate builds?
5. **Offline mode** — Cache schedule/attendance locally? How stale is acceptable?
6. **Language** — Arabic only? Or bilingual like the web?
......@@ -2,6 +2,7 @@
namespace App\Livewire\POS;
use App\Domain\Financial\Models\InvoiceItem;
use App\Domain\Financial\Services\CashSessionService;
use App\Domain\Inventory\Models\Product;
use App\Domain\Participant\Models\Participant;
......@@ -49,6 +50,9 @@ class POSTerminal extends Component
// Participant search results
public array $searchResults = [];
// Essential product warnings (product was already purchased this year)
public array $essentialWarnings = [];
public function mount(CashSessionService $cashSessionService): void
{
$this->authorize('pos.sell');
......@@ -85,12 +89,18 @@ public function clearParticipant(): void
$this->participantId = null;
$this->participantName = '';
$this->searchResults = [];
$this->essentialWarnings = [];
}
public function addProduct(int $productId): void
{
$product = Product::findOrFail($productId);
// Check if essential product was already purchased this year by this participant
if ($product->is_essential && $this->participantId) {
$this->checkEssentialProductHistory($product);
}
$existingIndex = collect($this->cart)->search(fn ($item) => $item['item_type'] === POSItemType::Product->value && $item['item_id'] === $product->id);
if ($existingIndex !== false) {
......@@ -114,6 +124,37 @@ public function addProduct(int $productId): void
];
}
private function checkEssentialProductHistory(Product $product): void
{
$yearStart = now()->startOfYear();
$productType = 'App\\Domain\\Inventory\\Models\\Product';
$previousPurchase = InvoiceItem::where('itemable_type', $productType)
->where('itemable_id', $product->id)
->where('created_at', '>=', $yearStart)
->whereHas('invoice', fn ($q) => $q
->where('billable_type', 'App\\Domain\\Participant\\Models\\Participant')
->where('billable_id', $this->participantId)
->whereNotIn('status', ['cancelled', 'draft'])
)
->orderByDesc('created_at')
->first();
if ($previousPurchase) {
$this->essentialWarnings[$product->id] = [
'product_name' => $product->name_ar,
'purchased_at' => $previousPurchase->created_at->format('Y-m-d'),
'participant_name' => $this->participantName,
];
$this->dispatch('essential-product-warning', productId: $product->id);
}
}
public function dismissEssentialWarning(int $productId): void
{
unset($this->essentialWarnings[$productId]);
}
public function addManualItem(): void
{
if (empty($this->manualItemName) || $this->manualItemPrice <= 0) {
......
......@@ -220,6 +220,28 @@ class="lg:hidden min-w-[44px] min-h-[44px] flex items-center justify-center text
</div>
</div>
{{-- Essential Product Warnings --}}
@if(count($essentialWarnings) > 0)
<div class="px-3 sm:px-4 pt-3 space-y-2">
@foreach($essentialWarnings as $productId => $warning)
<div class="flex items-start gap-2 p-3 bg-amber-50 border border-amber-300 rounded-lg">
<svg class="w-5 h-5 text-amber-600 shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"/>
</svg>
<div class="flex-1 min-w-0">
<p class="text-xs font-bold text-amber-800">{{ __('تنبيه: منتج أساسي مكرر') }}</p>
<p class="text-xs text-amber-700 mt-0.5">
{{ $warning['participant_name'] }} {{ __('اشترى') }} "{{ $warning['product_name'] }}" {{ __('بتاريخ') }} <span dir="ltr" class="font-medium">{{ $warning['purchased_at'] }}</span>
</p>
</div>
<button wire:click="dismissEssentialWarning({{ $productId }})" class="text-amber-500 hover:text-amber-700 shrink-0">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
@endforeach
</div>
@endif
{{-- Cart Items --}}
<div class="flex-1 overflow-y-auto p-3 sm:p-4 space-y-2 sm:space-y-3" wire:loading.class="opacity-50 pointer-events-none" wire:target="removeItem,updateQuantity">
@forelse($cart as $index => $item)
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment