Brandessence Backend
Technical PRD
Architecture Overview
InfraOne Laravel application. One PostgreSQL database. Two Filament panels. All tables live together with proper foreign key constraints. pgvector enabled from day one for future AI features.
Panel 1 — Customer Portal
- app.brandessence.com
- Filament customer panel
- Public register / login via OTP
- Purchase reports + manage licenses
- Livewire reactive checkout
- Dashboard, downloads, samples
Panel 2 — Admin + CRM
- admin.brandessence.com
- Single panel, role-based nav
- Super Admin → everything
- Report Author → own reports
- Sales Manager → CRM sections
- Research Analyst → tickets only
REST API
- /api/* — public endpoints
- No auth required
- Consumed by Next.js frontend
- Cached at Cloudflare edge
- Reports, catalogue, form submissions
- Cache-Control headers on all GET
crm_users table, same role system, same auth, and frequently reference the same data. A Sales Manager creating a manual order needs admin order management. One panel. Role-based sidebar. Clean and maintainable.
OTP Authentication
Customer PortalOTP Flow
Check if email exists in users table. New or existing — both proceed to OTP generation.
Key: otp:{email} — Value: {code, attempts: 0} — TTL: 600 seconds. Any existing OTP invalidated.
Dispatched as SendOtpEmail queue job. User sees OTP input + profile fields (first purchase) or OTP only (returning).
Valid → create session, redirect to intended page. Invalid → increment attempts. 3 failures → invalidate, must request new.
Redis session driver. 30-day duration with activity. Max 2 concurrent sessions (1 desktop + 1 mobile). 3rd login = oldest terminated. Device detected via User-Agent.
Required: email, first_name, last_name, company, phone, country. Optional: job_title. Account created with ALL fields simultaneously after OTP verification. No separate registration-then-checkout flow.
Only email required. OTP verifies. Account created with email only. Profile fields filled later from dashboard if they choose.
Cart System
Customer PortalDatabase-backed cart for logged-in users. One cart per user. Cart items reference reports. Prices always pulled from the database — never from query parameters or frontend.
Schema
Coupon Validation — 6 Checks (in order)
"Invalid coupon code"
"This coupon has expired"
"This coupon is no longer available"
Check coupon_usage table — "You've already used this coupon"
"Minimum order of $X required"
Scope: all / specific_industries / specific_reports — "This coupon doesn't apply to items in your cart"
Checkout Flow
Customer PortalMulti-step Livewire component. Each step validates before allowing progression to the next.
Auth Check
Redirect to OTP login with return URL if unauthenticated.
Order Review
Prices recalculated from DB. Coupon field. Show breakdown: Subtotal / Discount / Total.
License + Billing
Licensee name, company, billing address, GST (required if India).
Gateway Select
CCAvenue (Indian) or PayPal (International).
Payment
Create order (pending), process via gateway, verify, dispatch OrderCompleted event.
Confirmation
Redirect to /order-confirmation/{order_number} with download links.
flagged_review status → DO NOT fulfill → alert PM immediately.
OrderCompleted Event Listeners
- CreateLicenseRecords — creates license record per order item with type, user, report, expiry
- DispatchWatermarkJob — queues PDF watermarking per report. Text:
{Name} | {Company} | {OrderNum} | {Date} | Confidential - SendOrderConfirmationEmail — ZeptoMail with order details + download links (or "processing")
- UpdateCRMLead — if buyer email matches a lead → move to "Won", record order_id, attribute revenue
- SyncTypesenseIndex — update report's purchase_count for popularity sort
- LogAnalyticsEvent — store purchase event for admin dashboard
CCAvenue Integration
order_id, amount, currency (INR), redirect_url, cancel_url, merchant_id
Server-side only using CCAvenue working key. NEVER expose working key to frontend.
User completes payment on hosted page.
Decrypt response → verify order_id + amount match DB → set status accordingly.
CCAvenue sends callback regardless of user redirect. Handles browser-close edge case. Ignore if already completed.
PayPal Integration
Livewire component renders PayPal button inline.
POST /api/paypal/create-order — Laravel calls PayPal /v2/checkout/orders, returns PayPal order ID to SDK.
SDK calls POST /api/paypal/capture-order.
Laravel calls PayPal capture API → verify amount matches DB total → dispatch OrderCompleted event or flag.
License System
Customer PortalOne named user (purchaser). Unlimited re-downloads. Watermark: Name | Company | OrderNum | Date | Confidential
Company-level. N downloads (admin configures per report, e.g. 5 or 10). Counter only — no seat tracking. Limit reached → "Contact support."
Organisation-level. Unlimited downloads. No seat tracking. Watermark includes "Enterprise License".
ReportAccessService::checkAccess()
// Access check priority order: 1. No user → preview only 2. Direct purchase license → full access (check is_active + expiry) 3. Industry subscription → full access (check pivot + expiry) 4. Neither → preview only // Called by: → Report detail API endpoint (how much data to return) → Download controller (allow download?) → Customer dashboard (which reports in "My Reports")
Secure File Delivery
Customer PortalR2 Bucket Structure
/reports/{report_id}/original.pdf ← master, never served directly
/reports/{report_id}/excel.xlsx
/reports/{report_id}/ppt.pptx
/reports/{report_id}/watermarked/{license_id}.pdf ← per-license
/samples/{ticket_id}/sample.pdf
Download Flow
Controller calls ReportAccessService::checkAccess()
Return 403 — "You don't have access to this report"
Does /watermarked/{license_id}.pdf exist?
Generate signed URL (60-min expiry via S3-compatible API). Increment license.download_count. Log to download_logs. Return URL.
Dispatch high-priority watermark job. Return "Your download is being prepared. You'll receive an email shortly."
Chapter-Level Purchasing
Customer PortalBuyer can purchase individual chapters or multiple. Watermarked PDF generated containing only purchased chapters (extracted from master PDF).
Cart URL format: app.brandessence.com/cart/add/{report_id}?type=chapter&chapters=5,6,7,8
License chapters field = JSON array of purchased chapter IDs. Access check: does chapters array contain the requested chapter_id?
Subscription System
Customer PortalRenewal Notification Schedule
- 30 days before — "Your subscription expires in 30 days"
- 14 days before — "Your subscription expires in 14 days"
- 7 days before — "Renew now"
- On expiry — set
is_active = false, send "expired" email - 7 days after — "We miss you — renew to restore access"
Customer Dashboard Pages
Customer PortalSummary cards (purchases, active subs, saved, samples received). Recent activity log — last 10 entries.
Grid of all reports via license OR subscription. Download PDF/Excel/PPT. Chapters view — buy remaining. Filter: All | Full | Chapters | Add-ons.
Full interactive report viewer — Livewire page. All charts with complete data via ApexCharts. All TOC chapters unlocked. Full segmentation + clickable country map.
Sample deliveries. Signed URL per sample. "Buy Full Report" → cart link.
Purchase history table. Invoice PDF download. Order detail line items.
Edit profile. Read-only email. Saved billing addresses. GST number. "Logout all devices" button. Email notification preferences.
Admin + CRM Panel Structure
Admin Panelauthor_id.Report Management — 12-Tab Form
Admin PanelThe most complex admin feature. Filament Resource with tabbed form. All chart data saved as JSON in report_charts table.
title, slug, short_description, full_description (rich text), report_code, publisher_name
industries (multi-select pivot), geographies (hierarchical), report_type, methodology, forecast_period, base_year
single/multi/enterprise prices. enterprise_enquiry toggle. excel_price, ppt_price. multi_user_download_limit.
Repeater field. chapter_number, title, page_count, price, is_gated. Drag-to-reorder.
Live preview for: market size bar, regional pie/donut, CAGR line, market share horizontal bar, segment stacked, interactive country map.
market_size_value, market_size_year, cagr_percentage, forecast_period_display, companies_covered
Repeater: company_name, logo (R2 upload), hq_country, description. First 5 free / rest gated.
Dimension name + sub_segments per dimension. e.g. "By Type", "By Application".
seo_title (max 70), meta_description (max 160), canonical_url, og_image, no_index, focus_keywords
Repeater: question + rich text answer.
report_pdf (max 100MB → R2), excel_file (50MB, optional), ppt_file (100MB, optional)
enum: draft | published | unpublished | scheduled. published_at timestamp for scheduling.
report_charts table schema
Order Management + Manual Orders
Admin PanelOrder list with filters for status, gateway, date range, type. CSV export. Manual order creation for Super Admin and Sales Manager allows custom pricing and offline payments.
Manual Order Fields
- Customer: search existing by email, or create new (email → OTP registration)
- Items repeater: report, license type, price override (overrides catalogue price), excel/ppt toggles
- Payment method: Online / Wire Transfer / Cheque / Payment Terms
- Payment status: Paid (offline) / Invoice Sent / Pending
- Payment terms: Immediate / Net-15 / Net-30 / Net-60
- Access grant: Immediate → creates licenses now. On Payment Confirmation → licenses with
is_active = false, activated when admin marks paid. - CRM link: searchable select to link to lead → moves lead to "Won", attributes revenue
CRM — Lead Management
CRM13 classification fields per lead. Every action creates an activity log entry building the chronological timeline on lead detail page.
- website_sample
- website_contact
- website_enterprise
- website_newsletter
- manual (admin-created)
- csv_import
Manually set by sales rep:
hot warm cold
Default on creation from website: warm
13 Classification Fields
Duplicate Detection
leads table for matching email. If match found → update existing lead with new data (if fields were empty) → log activity: "Duplicate inquiry merged — {source}" → If sample request: create ticket linked to existing lead.
CRM — Pipeline Kanban
CRMLivewire Kanban component. Drag-drop between stages updates lead.pipeline_stage_id and creates activity log entry.
order_id, attribute revenue to sales rep.
CRM — Sample Request Ticketing
CRMTicket number format: SR-2026-0001 (auto-generated). 5-stage workflow from submitted to shared.
Auto from website form or manually by sales rep. Research Manager notified.
Research Manager sets assigned_to (analyst) and due_date. Analyst notified.
Analyst changes status. Can add comments (page refresh, no real-time WebSocket in Phase 1).
Analyst uploads file to R2. New upload replaces previous (old deleted). Sales rep notified.
"Publish to Dashboard" → creates sample_delivery record, sends OTP access link via email. OR "Send via Email" → manual, mark as shared. Lead auto-moves to "Sample Shared" stage.
REST API — Endpoints for Next.js
Public APIAll GET endpoints are public, no auth required, cached at Cloudflare edge. POST endpoints: no-store.
Reports & Catalogue
Form Submissions
Queue Jobs & Scheduled Commands
InfraQueue Jobs (Redis-backed)
/watermarked/{license_id}.pdfScheduled Commands (Cron via Laravel Scheduler)
// config/console.php or Kernel.php daily at 00:00 CheckSubscriptionExpiry ← expire subs, send notifications daily at 00:30 SendSubscriptionReminders ← 30/14/7 day warnings daily at 02:00 ReconcileOrders ← check orphaned pending orders with gateways daily at 03:00 ExpireFailedOrders ← expire pending/failed > 24 hours
Supervisor Workers
# Default queue: 2-4 workers php artisan queue:work redis --sleep=3 --tries=3 --max-time=3600 # High priority (watermarking + payments): 1 dedicated worker php artisan queue:work redis --queue=high,default --sleep=3 --tries=3
Typesense Indexing
InfraLaravel Scout with Typesense driver. Auto-sync on report CRUD. Full reindex available via php artisan scout:import "App\Models\Report"
Collection: reports — Key Fields
Email Templates — ZeptoMail
Infra"Your verification code is {code}. Expires in 10 minutes."
Order details, items, license type, download links (or "processing" if watermarking still in progress)
"Your sample for {report} is ready. Click to access." — OTP access link.
Sent by sales rep manually with attachment — template for consistency
30 / 14 / 7 day variants + expiry notification + post-expiry "we miss you" re-engagement
Sent on first registration (after first checkout or first sample OTP)
"Your report is ready for download" — sent when async watermarking completes for first download
Build Timeline — 28 Blocks
PlanningActive development: Weeks 3–25 (~22 weeks). Complexity ratings guide prioritisation and sprint planning.