Skip to content

Work / Seedha Ghar / Wiki / Flows

load-bearing-flows

Flowcanonicalverified 2026-07-22

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 ../product/catalog.md.


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

Screens: S10 → S11a → (S11b conditional) → S11c → S12 → S13

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 (S14).
  - 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: S34 → (S35 or S36) → S34b

buyer taps "Unlock owner contact · ₹5" on S34
  → 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 S36 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 S34b, 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: S15 (live) → S16 (expired) → seller taps Renew · ₹5 → S13

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 S16):
  → 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: A01 → A02 → A03 → seller S13 (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 A03 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 (S14)

  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: S38 (buyer) → A04 → A05 → refund + optional listing removal

buyer files complaint (from S34b or S38)
  → 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 (A05)
  → 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 A07)
      - 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 via Supabase Auth; documented in step-4 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 A03; treated as an operational tool, not a load-bearing user flow.