Skip to content

Work / Seedha Ghar / Wiki / Concepts

load-bearing-flows

Conceptcanonicalverified 2026-10-23

FLOW.LOAD-BEARING-FLOWS

Flows — Load-bearing sequence diagrams

Five flows the architecture must survive. If any of these is wrong, the product is broken. Everything else in the app is CRUD.

Prices, states, and SLAs come from catalog. Screens are named by their wireframe file, e.g. seller/home is .context/designs/mobile/seller/home.html (admin screens are web only). The sequences describe the proposed backend in architecture-overview; none of it is built, and the web app runs on seed data.


1. Publish listing (seller pays ₹5, enters review)

Screens: seller/home → seller/new-listing-basics → (seller/agent-noc conditional) → seller/new-listing-location → seller/new-listing-photos → seller/listing-pending

seller → apps/mobile (or apps/web)
  ├─ 1. Draft: POST /listings           → 201 { id, state=draft }
  ├─ 2. PATCH /listings/:id             (basics, then location, then photos)
  ├─ 3. Photo upload: POST /listings/:id/photos
  │      → api: signed R2 upload URL → mobile uploads bytes direct to R2
  │      → api: INSERT photo row (grade=null pending worker)
  │      → enqueue reverse-image-search + resize workers
  ├─ 4. Submit: POST /listings/:id/submit
  │      begin transaction
  │        SELECT wallet FOR UPDATE
  │        IF balance < 500: rollback → 402 (recharge required)
  │        INSERT wallet_ledger(delta=-500, reason='publish')
  │        UPDATE wallet.balance -= 500
  │        INSERT listing_version(snapshot, review_state='pending')
  │        UPDATE listing.state = 'in_review'
  │      commit
  │      → enqueue notify(reviewer_queue)
  │      → 200
  └─ 5. State poll or WebSocket: GET /listings/:id → state='in_review'

Error paths:
  - Photo grade='stolen_or_stock' worker sets listing.review_state='needs_changes'
    with reason='photo:stolen'; state transitions to 'needs_changes' visible to seller (seller/listing-needs-changes).
  - Wallet debit fails after photo upload: photos survive as orphans, cleaned by
    weekly janitor cron. Wallet is never charged for a rejected upload.

Invariants

  • Exactly one wallet_ledger row per publish attempt (success or refund).
  • Reviewer never sees a listing without at least one internal photo (blocked in step 3 check).

2. Buyer unlock (₹5 → owner contact revealed)

Screens: buyer/property-detail → (buyer/unlock-confirm or buyer/recharge-gate) → buyer/property-detail?state=unlocked

buyer taps "Unlock owner contact · ₹5" on buyer/property-detail
  → apps/mobile → POST /unlocks { listing_id }
  → apps/api:
      begin transaction
        SELECT wallet FOR UPDATE where user_id=buyer
        IF balance < 500 (paise):
          rollback
          return 402 { code: 'RECHARGE_REQUIRED', wallet_balance }
          → client shows buyer/recharge-gate recharge gate, on success retries the POST /unlocks
        IF listing.state != 'live':
          rollback → 409 { code: 'LISTING_UNAVAILABLE' }
        IF exists(unlocks where buyer_id, listing_id):
          rollback → 200 { already_unlocked: true, contact } ← idempotent
        INSERT wallet_ledger(delta=-500, reason='unlock', ref=listing_id, balance_after)
        UPDATE wallet.balance -= 500
        INSERT unlocks(buyer_id, listing_id, price_paid=500)
        INSERT audit_events(actor=buyer, action='unlock', target=listing_id)
      commit
      → enqueue notify(seller: "buyer X unlocked", buyer_interest_delta+1)
      → return { contact: { name, phone, whatsapp } }
  → mobile transitions to buyer/property-detail, renders unlocked view

Refund path (buyer files scam complaint later):
  → admin resolves complaint with action='refund'
  → apps/api:
      begin transaction
        INSERT wallet_ledger(delta=+500, reason='refund_unlock', ref=unlock_id)
        UPDATE wallet.balance += 500
        UPDATE unlocks.refunded_at, refund_reason
        INSERT audit_events(actor=admin, action='refund', target=unlock_id)
      commit
      → notify(buyer)

Idempotency: repeated POST /unlocks for the same (buyer, listing) never double-charges. Enforced by unique index on (buyer_id, listing_id) where refunded_at IS NULL.


3. Thirty-day auto-takedown (cron)

Screens: seller/listing-live (live) → seller/listing-paused (expired) → seller taps Renew · ₹5 → seller/listing-pending

Cloudflare Cron Trigger: every hour at :00
  → apps/api /cron/expire-listings (auth: cron secret)
  → SELECT id FROM listings
     WHERE state='live' AND expires_at < now()
     LIMIT 500                            ← batch to stay under Worker CPU cap
  → for each: UPDATE listing.state='expired'
             INSERT audit_events(actor='system', action='auto_expire', target=listing)
             enqueue notify(seller: "listing expired, renew for ₹5")
  → if 500 returned, next tick picks up the rest (idempotent)

Renewal (user-triggered from seller/listing-paused):
  → POST /listings/:id/renew
  → same money+version+state transition as publish (§1),
    but transitions state 'expired' → 'in_review',
    creates new listing_version (reviewer sees the delta vs last approved version)

Invariants

  • listing.expires_at = last_published_at + 30 days recomputed on every publish and renewal.
  • Renewals go through review — auto-approve is forbidden.

4. Reviewer verification queue

Screens: admin/home → admin/verification-queue → admin/verification-detail → seller/listing-pending (updates)

reviewer opens /admin/queue?type=verification
  → apps/api /admin/queue
      SELECT listing_versions
      WHERE review_state='pending'
      ORDER BY submitted_at ASC                        ← FIFO for fair SLA
      + join photo grades, agent NoC, wallet history
  → reviewer picks one, opens admin/verification-detail
  → reviewer sets:
      - reviewer_title (crafted from structured fields)
      - polygon_id (from library or draws new — kicks off polygon-create sub-flow)
      - visibility_mask (per-field overrides on top of CATALOG defaults)
      - decision: approve | needs_changes | reject
  → POST /admin/listings/:id/approve
      begin transaction
        UPDATE listing_versions SET review_state='approved', reviewed_by, reviewed_at
        UPDATE listings SET
          state='live',
          published_at=now(),
          expires_at=now()+30d,
          reviewer_title, polygon_id, visibility_mask,
          current_version_id
        INSERT audit_events(actor=reviewer, action='approve', target, before, after)
      commit
      → enqueue notify(seller: "listing live")

  needs_changes path:
      UPDATE listing.state='needs_changes' + note-list
      → notify seller with the note list (seller/listing-needs-changes)

  reject path:
      UPDATE listing.state='removed_by_admin'
      wallet stays debited (fee is for review, not for approval)
      audit_events records reason

Invariants

  • Reviewer decisions are always via /admin/* handlers — never direct DB.
  • SLA is measured from submitted_at to reviewed_at, target 48h (see CATALOG § Verification SLA).
  • Every field on visibility_mask toggle is logged in audit_events.before/after — a reviewer who over-shares can be identified.

5. Complaint → refund → optional kill-listing

Screens: buyer/report-listing (buyer) → admin/complaints-queue → admin/complaint-detail → refund + optional listing removal

buyer files complaint (from an unlocked buyer/property-detail or buyer/report-listing)
  → apps/mobile → POST /complaints { unlock_id?, listing_id, severity, body }
  → apps/api:
      INSERT complaints(filer_id, severity, state='open', ...)
      INSERT audit_events(actor=buyer, action='file_complaint')
      → if severity in ('NCDRC','DPDP','RERA'): enqueue notify(admin_pager)
      → return 201

admin opens /admin/complaints/:id (admin/complaint-detail)
  → chooses action:
      - refund: as in §2 refund path
      - kill_listing: state='removed_by_admin', notify seller, no wallet impact
      - warn_user: adds warning row to user profile (visible on admin/user-detail)
      - none: mark 'dismissed' with reason
  → POST /admin/complaints/:id/resolve { action, notes }
      begin transaction
        UPDATE complaints SET state='resolved', resolved_at, action_taken, assigned_to
        (execute action side-effects — refund and/or kill in same tx)
        INSERT audit_events(actor=admin, action='resolve_complaint', ...)
      commit
      → notify(buyer + seller if kill)

SLA (from CATALOG)

  • Acknowledge < 72 hrs (auto-ack email on POST /complaints).
  • Action < 7 days.
  • Refund processing 7 days from documented complaint.

Regulatory backstop

  • Complaints with severity in ('NCDRC','DPDP','RERA') cannot be dismissed without a reason string and a linked grievance-officer email thread. Enforced in handler.

What's deliberately NOT flowed here

  • Auth / OTP — standard phone-OTP; the auth provider is not chosen (tech-stack).
  • Recharge — standard Razorpay Payment Link → webhook → credit ledger. See flow 2 for the RECHARGE_REQUIRED handoff.
  • Photo upload — covered inside flow 1 step 3; no separate flow.
  • Favorites, filters, notifications, settings — CRUD, no invariant risk.
  • Polygon creation — reviewer sub-flow inside admin/verification-detail; treated as an operational tool, not a load-bearing user flow.