Email System Fixes:
- Fix email sending after payment: handle-order-paid now calls send-notification
instead of send-email-v2 directly, properly processing template variables
- Fix order_created email timing: sent immediately after order creation,
before payment QR code generation
- Update email templates to use short order ID (8 chars) instead of full UUID
- Add working "Akses Sekarang" buttons to payment_success and access_granted emails
- Add platform_url column to platform_settings for email links
OTP Verification Flow:
- Create dedicated /confirm-otp page for users who close registration modal
- Add link in checkout modal and email to dedicated OTP page
- Update OTP email template with better copywriting and dedicated page link
- Fix send-auth-otp to fetch platform settings for dynamic brand_name and platform_url
- Auto-login users after OTP verification in checkout flow
Admin Features:
- Add delete user functionality with cascade deletion of all related data
- Update IntegrasiTab to read/write email settings from platform_settings only
- Add test email template for email configuration testing
Cleanup:
- Remove obsolete send-consultation-reminder and send-test-email functions
- Update send-email-v2 to read email config from platform_settings
- Remove footer links (Ubah Preferensi/Unsubscribe) from email templates
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The replaceVariables function was only supporting {{key}} format but
the email templates use {key} format (single braces). Updated to support
both formats for compatibility.
Changes:
- Added support for {key} format in addition to {{key}}
- Ensures all template variables are properly replaced
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The send-notification function was using SMTP by default and failing with
ConnectionRefused errors. Added Mailketing API integration to match the
email provider used by other functions (send-auth-otp, send-email-v2).
Changes:
- Added sendViaMailketing() function with form-encoded body
- Added "mailketing" case to provider switch (now the default)
- Changed default provider from "smtp" to "mailketing"
- Reads API token from mailketing_api_token or api_token settings
This ensures order_created emails are sent successfully via Mailketing
just like other email notifications in the system.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Updated to use the correct database column names:
- email_subject (not subject)
- email_body_html (not body_html)
Changes:
- send-notification/index.ts: Added fallback to check both email_subject/subject
and email_body_html/body_html for compatibility
- Migration: Updated to use correct column names (email_subject, email_body_html)
This matches the actual database schema shown in the notification_templates table.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
The function was using .eq("template_key") but the actual column name
in notification_templates table is "key". This caused "Template not found"
errors even when the template existed and was active.
Changes:
- send-notification/index.ts: Changed .eq("template_key") to .eq("key")
- Matches the pattern used in send-auth-otp and other edge functions
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Enhanced email notification system to send order confirmation emails immediately after order creation with embedded QR code for payment.
Changes:
- Checkout.tsx: Added send-notification call after payment creation with comprehensive logging
- send-notification: Added QRCode library integration for generating base64 QR images for order_created emails
- NotifikasiTab.tsx: Added QR code section to default order_created template and updated shortcodes list
Technical details:
- QR code generated as base64 data URL for email client compatibility
- Fire-and-forget pattern ensures checkout flow isn't blocked
- Added detailed console logging for debugging email send issues
- New shortcodes: {qr_code_image}, {qr_expiry_time}
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Create supabase/shared/email-template-renderer.ts for code reuse
- Update send-auth-otp to import from shared file (eliminates 260 lines of duplication)
- Add isResendOTP state to track existing user email confirmation
- Update login error handler to detect unconfirmed email
- Show helpful message when user tries to login with unconfirmed email
This addresses:
1. Code duplication between src/lib and edge functions
2. User experience for unconfirmed email login attempts
- Add EmailTemplateRenderer class to send-auth-otp edge function
- Wrap OTP email content in master template with brutalist design
- Email now includes proper header, footer, and styling
- No changes needed to checkout flow (uses auth page for registration)
Benefits:
- Professional branded emails with ACCESS HUB header
- Consistent brutalist design across all emails
- Responsive layout
- Better email client compatibility
- Remove notification_logs table references (table doesn't exist)
- This was causing the function to crash after sending email
- Now the function should complete successfully
- Added better email payload logging
- Keep .env file for local development
- Add "Catatan" field display in consulting order detail page
- Add dedicated "Cancelled Order" section with rebooking option
- Update status alert to show proper message for cancelled orders
- Refactor edge function to focus on calendar cleanup only
- Set payment_status to 'failed' when auto-cancelling expired orders
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
**Features Implemented:**
1. **Auto-Cancel Expired Consulting Orders:**
- New edge function: cancel-expired-consulting-orders
- Changes order status from 'pending' → 'cancelled'
- Cancels all associated consulting_sessions
- Deletes calendar events via delete-calendar-event
- Releases consulting_time_slots (deletes booked slots)
- Properly cleans up all resources
2. **Smart Re-Booking with Pre-filled Data:**
- OrderDetail.tsx stores expired order data in sessionStorage:
- fromExpiredOrder flag
- Original orderId
- topicCategory
- notes
- ConsultingBooking.tsx retrieves and pre-fills form on mount
- Auto-clears sessionStorage after use
3. **Improved UX for Expired Orders:**
- Clear message: "Order ini telah dibatalkan secara otomatis"
- Helpful hint: "Kategori dan catatan akan terisi otomatis"
- One-click re-booking with pre-filled data
- Member only needs to select new time slot
**How It Works:**
Flow:
1. QRIS expires → Order shows expired message
2. Member clicks "Buat Booking Baru"
3. Data stored in sessionStorage (category, notes)
4. Navigates to /consulting
5. Form auto-fills with previous data
6. Member selects new time → Books new session
**Edge Function Details:**
- Finds orders where: payment_status='pending' AND qr_expires_at < NOW()
- Cancels order status
- Cancels consulting_sessions
- Deletes consulting_time_slots
- Invokes delete-calendar-event for each session
- Returns count of processed orders
**To Deploy:**
1. Deploy cancel-expired-consulting-orders edge function
2. Set up cron job to run every 5-15 minutes:
`curl -X POST https://your-domain/functions/v1/cancel-expired-consulting-orders`
**Benefits:**
✅ Orders properly cancelled when QR expires
✅ Time slots released for other users
✅ Calendar events cleaned up
✅ Easy re-booking without re-typing data
✅ Better UX for expired payment situations
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Add availability checking and calendar sync to admin session editing:
**New Features:**
- Admin can now select time slots using visual picker with availability checking
- Time slot picker respects confirmed sessions and excludes current session from conflict check
- Calendar events are automatically updated when session time changes
- consulting_time_slots table is updated when time changes (old slots deleted, new slots created)
**New Component:**
- src/components/admin/TimeSlotPickerModal.tsx
- Reusable modal for time slot selection
- Shows visual grid of available time slots
- Range selection for multi-slot sessions
- Availability checking against consulting_sessions
- Supports editing (excludes current session from conflicts)
**Enhanced AdminConsulting.tsx:**
- Replaced simple time inputs with TimeSlotPickerModal
- Added state: timeSlotPickerOpen, editTotalBlocks, editTotalDuration
- Added handleTimeSlotSelect callback
- Enhanced saveMeetLink to:
- Update consulting_time_slots when time changes
- Call update-calendar-event edge function
- Update calendar event time via Google Calendar API
- Button shows selected time with duration and blocks count
**New Edge Function:**
- supabase/functions/update-calendar-event/index.ts
- Updates existing Google Calendar events when session time changes
- Uses PATCH method to update event (preserves event_id and history)
- Handles OAuth token refresh with caching
- Only updates start/end time (keeps title, description, meet link)
**Flow:**
1. Admin clicks "Edit" on session → Opens dialog
2. Admin clicks time button → Opens TimeSlotPickerModal
3. Admin selects new time → Only shows available slots
4. On save:
- consulting_sessions updated with new time
- Old consulting_time_slots deleted
- New consulting_time_slots created
- Google Calendar event updated (same event_id)
- Meet link preserved
**Benefits:**
- ✅ Prevents double-booking with availability checking
- ✅ Visual time slot selection (same UX as booking page)
- ✅ Calendar events stay in sync (no orphaned events)
- ✅ Time slots table properly maintained
- ✅ Meet link and event_id preserved during time changes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
**Issue 1: Fix end time display in booking summary**
- Now correctly shows start_time + slot_duration instead of just start_time
- Example: 09:30 → 10:00 for 1 slot (30 mins)
**Issue 2: Confirm create-google-meet-event uses consulting_sessions**
- Verified: Function already updates consulting_sessions table
- The data shown is from OLD consulting_slots table (needs migration)
**Issue 3: Delete calendar events when order is deleted**
- Enhanced delete-order function to delete calendar events before removing order
- Calls delete-calendar-event for each session with calendar_event_id
**Issue 4: Admin can now edit session time and manage calendar events**
- Added time editing inputs (start/end time) in admin dialog
- Added "Delete Link & Calendar Event" button to remove meet link
- Shows calendar event connection status (✓ Event Kalender: Terhubung)
- "Regenerate Link" button creates new meet link + calendar event
- Recalculates session duration when time changes
**Issue 5: Enhanced calendar event description**
- Now includes: Kategori, Client email, Catatan, Session ID
- Format: "Kategori: {topic}\n\nClient: {email}\n\nCatatan: {notes}\n\nSession ID: {id}"
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Migrate consulting_slots to consulting_sessions structure
- Add calendar_event_id to track Google Calendar events
- Create delete-calendar-event edge function for auto-cleanup
- Add "Tambah ke Kalender" button for members (OrderDetail, ConsultingHistory)
- Update create-google-meet-event to store calendar event ID
- Update handle-order-paid to use consulting_sessions table
- Remove deprecated create-meet-link function
- Add comprehensive documentation (CALENDAR_INTEGRATION.md, MIGRATION_GUIDE.md)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add pending slot state to distinguish between selected and confirmed slots
- First click: slot shows as pending (amber) with "Pilih" label
- Second click (same slot): confirms single slot selection
- Second click (different slot): creates range from pending to clicked slot
- Fix "Body already consumed" error in OAuth token refresh
- Enhance admin consulting slot display with category and notes
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes:
1. AdminConsulting.tsx: Update createMeetLink to call Supabase edge function
- Remove dependency on n8n webhook URL
- Call create-google-meet-event edge function directly
- Use environment variables for Supabase URL and anon key
- Improve error handling and user feedback
2. handle-order-paid: Add comprehensive error logging
- Log meet response status
- Log full response data
- Log errors when meet_link update fails
- Log error response text when request fails
- Better debugging for troubleshooting meet creation issues
This fixes:
- CORS issues when calling n8n webhook
- 404 errors from deleted /webhook-test/create-link endpoint
- Manual meet link creation now uses same flow as automatic
- Better visibility into meet creation failures via logs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Changes:
- Sort consulting slots by start_time before processing
- Ensures correct first/last slot selection for calendar events
- Add debug logging to track time slot calculations
- Fixes end time calculation for multi-slot consulting orders
This ensures that when multiple slots are booked:
- Slots are processed in chronological order
- Calendar event uses first slot's start and last slot's end
- Event duration correctly covers all booked slots
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Calendar Timezone Fix:
- Add +07:00 timezone offset to date strings in create-google-meet-event
- Fixes 13:00 appearing as 20:00 in Google Calendar
- Now treats times as Asia/Jakarta time explicitly
Single Calendar Event per Order:
- handle-order-paid now creates ONE event for all slots in an order
- Uses first slot's start time and last slot's end time
- Updates all slots with the same meet_link
- Prevents duplicate calendar events for multi-slot orders
Admin Consulting Page Improvements:
- Group consulting slots by order_id
- Display as single row with continuous time range (start-end)
- Show session count when multiple slots (e.g., "2 sesi")
- Consistent with member-facing ConsultingHistory component
- Updated both desktop table and mobile card layouts
- Updated both upcoming and past tabs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Fix consulting history to show continuous time range (09:00 - 11:00) instead of listing individual slots
- Add foreign key relationships for consulting_slots (order_id and user_id)
- Fix handle-order-paid to query profiles(email, name) instead of full_name
- Add completed consulting sessions with recordings to Member Access page
- Add user_id foreign key constraint to consulting_slots table
- Add orders foreign key constraint for consulting_slots relationship
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Added detailed logging to diagnose why consulting slots aren't being updated:
- Log order details including consulting_slots count
- Log whether order is detected as consulting order
- Log slot update result and any errors
This will help identify where the process is failing.
The database trigger approach wasn't working because the trigger either doesn't
exist or the required database settings (app.base_url, app.service_role_key) aren't
configured.
This is a simpler, more reliable solution:
- pakasir-webhook updates order to 'paid' status
- pakasir-webhook then directly calls handle-order-paid edge function
- No dependency on database triggers or settings
This matches how other parts of the system work - direct function calls with
environment variables, not database triggers.
Critical bug fix: Consulting orders were not being processed after payment because
the function checked order_items for consulting products, but consulting orders
don't have order_items - they have consulting_slots instead.
Changes:
- Fetch consulting_slots along with order_items in the query
- Check for consulting_slots.length > 0 to detect consulting orders
- Update consulting_slots status from 'pending_payment' to 'confirmed'
- Create Google Meet events for each consulting slot
- Send consulting_scheduled notification
This fixes the issue where:
- Consulting slots stayed in 'pending_payment' status after payment
- No meet links were generated
- No access was granted
- Schedules didn't show up in admin or member dashboard
- Remove PayPal payment option from checkout
- Add qr_string and qr_expires_at columns to orders table
- Update create-payment to store QR string in database
- Update pakasir-webhook to clear QR string after payment
- Simplify Checkout to redirect to order detail page
- Clean up unused imports and components
Flow:
1. User checks out with QRIS (only option)
2. Order created with payment_method='qris'
3. QR string stored in database
4. User redirected to Order Detail page
5. QR code displayed in-app with polling
6. After payment, QR string cleared, access granted
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Rename function to abstract payment provider details
- Add support for both QRIS and PayPal methods
- Update frontend to use generic create-payment function
- Remove provider-specific naming from UI/UX
- Payment provider (Pakasir) is now an implementation detail
Response format:
- QRIS: returns qr_string for in-app display, payment_url as fallback
- PayPal: returns payment_url for redirect
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Create create-pakasir-payment edge function to handle payment creation server-side
- Update ConsultingBooking.tsx to use edge function instead of direct API call
- Update Checkout.tsx to use edge function instead of direct API call
- Add config.toml entry for create-pakasir-payment function
- Removes CORS errors when calling Pakasir API from frontend
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add delete button to AdminOrders dialog with Trash2 and AlertTriangle icons
- Create delete-order edge function to handle deletion requests
- Add database migration for delete_order function with comprehensive cleanup
- Update config.toml to register delete-order edge function
- Deletion sequence: reviews → consulting slots → order items → user access → order
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
BREAKING CHANGE: Complete refactor of payment handling
New Architecture:
1. pakasir-webhook (120 lines -> was 535 lines)
- Only verifies signature and updates order status
- Removed: SMTP, email templates, notification logic
2. Database Trigger (NEW)
- Automatically fires when payment_status = 'paid'
- Calls handle-order-paid edge function
- Works for webhook AND manual admin updates
3. handle-order-paid (NEW edge function)
- Grants user access for products
- Creates Google Meet events for consulting
- Sends notifications via send-email-v2
- Triggers webhooks
Benefits:
- Single Responsibility: Each function has one clear purpose
- Trigger works for both webhook and manual admin actions
- Easier to debug and maintain
- Reusable notification system
Migration required: Run 20241223_payment_trigger.sql
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Member OrderDetail page: Shows consulting slots with date/time and Join Meet button
- Admin Orders dialog: Shows consulting slots with meet link access
- Meet button only visible when payment_status is 'paid'
- Both pages show slot status (confirmed/pending)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When order is paid, automatically create Google Meet events for all consulting slots.
The meet_link is saved to consulting_slots table and included in notifications.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
Using req.text() first then parsing JSON gives us more control and avoids
stream consumption issues with Deno/Supabase edge functions.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Catch TypeError when req.json() is called on consumed stream
- Return success response for duplicate calls (first call handles the actual work)
- This handles React Strict Mode firing onClick twice in parallel
- Only the first call that successfully reads body will process the request
- Use req.clone().json() to handle multiple reads from same request
- This happens when React Strict Mode fires requests twice
- Cloning the request allows reading body multiple times safely
- Add expires_at timestamp to OAuth config
- Cache access_token in database to reuse across requests
- Only refresh token when it expires (after 1 hour)
- Use 60-second buffer to avoid using almost-expired tokens
- Auto-update cached token after refresh
- This fixes the invalid_grant error from excessive refresh calls
- Replace JWT service account authentication with OAuth2 refresh token flow
- Service accounts cannot create Google Meet links for personal Gmail accounts
- Update edge function to use OAuth2 token exchange
- Change database column from google_service_account_json to google_oauth_config
- Add helper tool (get-google-refresh-token.html) to generate OAuth credentials
- Update IntegrasiTab UI to show OAuth config instead of service account
- Add SQL migration file for new google_oauth_config column
OAuth2 Config format:
{
"client_id": "...",
"client_secret": "...",
"refresh_token": "..."
}
This approach works with personal @gmail.com accounts without requiring
Google Workspace or Domain-Wide Delegation.
- Try just 'hangoutsMeet' as type without 'name' field
- This is the most basic format according to some docs
- Combined with full event logging to debug
- Check conferenceData.entryPoints for video meet link
- Keep hangoutLink as fallback for backwards compatibility
- Log full event response to debug missing meet link
- This will show us what Google actually returns
- Remove conferenceSolutionKey entirely - let Google auto-select Meet
- Add console.log to see event data being sent
- Add response status logging for debugging
- Change type from 'hangoutsMeet' to 'event'
- Add name: 'hangoutsMeet' property
- This matches Google Calendar API requirements for creating Meet conferences
- Service accounts require Domain-Wide Delegation to invite attendees
- Removed attendees array, sendUpdates, and guest permissions
- Client email still included in event description
- Google Meet link will still be generated successfully
- Can re-enable attendees later if Domain-Wide Delegation is configured
- Remove external jose library dependency that was causing import errors
- Use native crypto.subtle API available in Deno
- Manual base64url encoding for JWT header, payload, and signature
- Use RSASSA-PKCS1-v1_5 with SHA-256 for RS256 algorithm
- Remove cat heredoc wrapper from file
- Build JWT header and payload manually for exact format control
- Use lower-level importKey/sign from Web Crypto API
- Use RSASSA-PKCS1-v1_5 algorithm directly (RSA+SHA256 = RS256)
- Manual base64url encoding for URL-safe tokens
- Add debug logging to trace JWT generation
- Avoids SignJWT abstraction that was causing algorithm errors
- Use importPKCS8 to convert private key string to CryptoKey
- Pass CryptoKey to SignJWT.sign() instead of string
- Fix 'Key for RS256 algorithm must be CryptoKey' error
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Change from jwt.sign() to SignJWT class
- Use proper jose library SignJWT API for Deno
- Fix 'Cannot read properties of undefined' error
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>