Qejaverse Feature Guide

Qejaverse Feature Guide

Open in ChatGPT
Ask ChatGPT about this page
Open in Claude
Ask Claude about this page

Qejaverse Project Anatomy

Components of a QejaVerse Project

Audience: new engineers, new AI agents, QA, and product.
Purpose: read this instead of reading the codebase. It explains every moving part of a Real Estate Project — what it is, why it exists, what it controls, and where it lives.

Last verified against code: 2026-07-29, branch crm-marketplace-split.
Companion document: QA Project Setup Test Plan.


1. The one-paragraph version

A Real Estate Project is the container for everything a developer sells. It is created by an activated Developer Profile, belongs to a Qejaverse Company (the developer's firm, or optionally a per-project SPV), and is filled in through an 8-step setup wizard. Its physical inventory is a four-level hierarchy — Phase → Building → Floor → Unit. Its commercial terms live in commission settings, payment plan templates and transaction fees. Its legitimacy is proven by due diligence documents. Its people are the Project Team. A weighted setup completeness score decides when it may be published; publishing makes it visible to agents. A separate gate — per unit-type — decides when it appears on the public marketplace.


2. Entity map

graph TD
  DP[Developer Profile<br/>must be is_activated] --> QC[Qejaverse Company<br/>type = Developer]
  QC --> REP[Real Estate Project]
  QC -.optional SPV.-> SPV[Qejaverse Company<br/>type = Project]
  SPV -.-> REP

  REP --> PH[Project Phase]
  PH --> PC[Property Container<br/>= Building / Block]
  PC --> PSC[Property Sub Container<br/>= Floor / Wing / Section]
  PSC --> PU[Project Unit]
  PC --> PU

  REP --> UTT[Unit Type Template]
  UTT -.applied to.-> PU
  UTT --> TM[Template Media]

  REP --> DD[Due Diligence Documents<br/>child table]
  REP --> AM[Project Amenity<br/>child table]
  REP --> EL[Project External Link<br/>child table]
  REP --> TF[Project Transaction Fee<br/>child table]
  REP --> PM[Project Media<br/>separate doctype]
  REP --> PPT[Payment Plan Template]
  PPT --> MS[Payment Milestone<br/>child table]
  REP --> OLT[Offer Letter Template<br/>= document templates]
  REP --> PT[Project Team]
  REP --> POR[Project Onboarding Record]
  REP --> PCU[Project Customer]

3. Before a project can exist — the preconditions

These are hard gates. Every one of them throws or redirects if unmet.

# Requirement Enforced where Failure symptom
1 User has a Developer Profile create_project_draft_logic 403 "You need a Developer Profile to create a project."
2 Developer Profile is_activated = 1 RealEstateProject.before_insert + wizard guard Redirect to /manager/company-profile with toast; API throws ValidationError
3 Profile has a Qejaverse Company (profile.company) create_project_draft_logic 400 "Finish setting up your developer company…"
4 Developer has accepted the Developer Platform Agreement (current version) Publish gate only (marketplace mode) Publish 400 with version numbers
5 Subscription project slot available doc_eventson_project_before_insert ValidationError "Subscription Limit Reached"

⚠️ Free tier on this site allows 1 project per user (Subscription Config.free_tier_project_limit = 1). Free tier also allows 1000 units, 10 buildings, 10 phases. QA needs either a paid subscription or one developer account per project.

Activation itself (out of scope here) creates the Developer-type Qejaverse Company synchronously, so profile.company is always populated by the time a project is created.


4. The Real Estate Project record

Doctype: Real Estate Project · naming PRJ-.#### · title field project_name
Controller: casahub_app/casahub_app/doctype/real_estate_project/real_estate_project.py
Main service: services/project_setup.py
API surface: casahub_app/casahub_app/api.py

4.1 Identity fields

Field What it is Rules
project_code Public-facing unique code, QVP-… Generated in before_insert via entity_codes.generate_entity_code("QVP"). Read-only. Disambiguates public URLs.
project_name Human name Required. Unique per firm, compared case- and whitespace-insensitively (firm_has_project_name). Two different firms may reuse a name. FE validation: 3–200 chars, must contain a letter.
slug Cosmetic URL slug Auto-derived from project_name if blank. Not unique — public URLs are disambiguated by project_code.
developer_profile Owning developer Required. Immutable after first save (validate_developer_profile_lock) — changing it would orphan clients, contracts and commissions.
developer_company The firm's Qejaverse Company Set once at creation, silently reverted if tampered with.
company Company of record for this project Defaults to developer_company; can be switched to a per-project SPV. See §5.
created_by Creator Read-only. Used by permission checks.

4.2 Classification fields

Field Source Why it matters
property_template System Options category Property Template Required. Drives the default transaction-fee set (get_default_kenyan_fees). Options: Residential Apartment, Residential Villa, Commercial Office, Commercial Retail, Mixed-Use, Townhouse, Villa, Commercial, Mixed.
project_category Registry Dictionary Concept class Project Categories Drives which due-diligence documents are required. Selectable: Residential, Commercial, Mixed-Use. Hospitality and Industrial exist as concepts but are not offered — see §6.
development_type Registry Dictionary Concept class Development Types New Construction, Renovation, Redevelopment, Expansion, High rise. Informational + completeness score.
project_status System Options category Project Status Planning, PreLaunch, Launched, Construction, Ready, Completed. Displayed publicly.
container_type System Options category Container Type Default building type. ⚠️ See known gap G-6.

4.3 Read-only rollups

total_units, available_units, number_of_phases are recomputed on every on_update by walking Project → Phase → Container → Unit. available_units counts units whose status option_value is available.

propagate_location_to_units copies city/state/country/lat/long/location down to every Project Unit via one bulk SQL UPDATE whenever project location changes — this is what makes marketplace geo-search work.


5. Company of record and the SPV option

Why it exists: developers often sell a project through a special-purpose vehicle (a separate registered company) rather than the parent firm. Contracts, invoices and offer letters must name the correct legal entity.

  • Default: project.company = project.developer_company (the firm).
  • Opt in on Step 1 of the wizard ("this project has its own company") or later at /projects/:id/manage/company.
  • create_project_company() creates a Qejaverse Company with company_type = "Project" and parent_company = <firm>. It reuses an existing same-named Project company under that firm rather than minting duplicates. Logo/country/currency fall back to the firm.
  • SPV legal fields: company name, logo, registration number, legal form, KRA PIN, date of incorporation, NCA reg no, VAT reg no, NEMA compliance, office address/phone/email, default bank name / account number / payment instructions.

Locks:

Condition Effect
Project is published or has any Project Customer The choice of company cannot change at all (_project_company_editable). We block; we never retroactively move records to a new entity.
Project has any Project Customer The SPV's identity fields (company_name, company_registration_number, kra_tax_pin, legal_form) are frozen — they appear on signed contracts. Other fields (address, bank, phone) stay editable.

6. Due diligence — proving the project is real

Child table: Project Due Diligence Document on the project
Catalogue: Due Diligence Document Type → child table Due Diligence Category Rule
Service: services/due_diligence_service.py

How requirements are derived

A document type is applicable to a project when it has a Due Diligence Category Rule row matching the project's project_category. That same row carries is_mandatory. So requirements are per project category, not global.

Current catalogue (12 types × 3 categories)

Seeded by casahub_app/casahub_app/setup/due_diligence_setup.py.

Document type Residential Commercial Mixed-Use Expires?
Title Deed / Lease Mandatory Mandatory Mandatory No
Approved Building Plans Mandatory Mandatory Mandatory No
NEMA EIA License Mandatory Mandatory Mandatory Yes
NCA Building Permit Mandatory Mandatory Mandatory Yes
County Development Permission Mandatory Mandatory Mandatory Yes
Land Search Certificate Mandatory Mandatory Mandatory Yes
Company Registration (CR12) Mandatory Mandatory Mandatory No
KRA Tax Compliance Certificate Mandatory Mandatory Mandatory Yes
Fire Safety Clearance Optional Mandatory Mandatory Yes
Survey Plan / Beacon Certificate Optional Optional Optional No
Power Supply Approval (KPLC) Optional Optional Optional No
Water & Sewerage Approval Optional Optional Optional No

Mandatory counts: Residential 8, Commercial 9, Mixed-Use 9. Mixed-Use takes the
Commercial obligations because a mixed-use development contains a commercial component
and so needs fire safety clearance.

Only these three categories are selectable. Because requirements come entirely
from category rules, a category with no rules would require nothing — so the wizard
offers only the seeded ones (SUPPORTED_PROJECT_CATEGORIES in
Casahub/src/services/projectSetup.js). Hospitality and Industrial still exist as
Registry Dictionary concepts (they remain valid developer specialisations) but
cannot be chosen for a project. To re-enable one, seed its rules in
due_diligence_setup.DOCUMENT_TYPES first, then add it to that constant. Projects
already saved under a retired category keep it, shown as "(no longer available)"
in the picker so it is never silently cleared.

Behaviour

  • save_due_diligence_documents_logic replaces the whole child table on every save. It re-derives is_mandatory from the category rules — a client cannot forge it.
  • A row whose expiry_date is in the past is stamped status = "Expired" on save.
  • due_diligence_status rollup: Complete (all mandatory have files, none expired) / Has Expired Documents / Incomplete.
  • Once the project is published, a mandatory document cannot have its file removed (validate_mandatory_docs_not_removed). Unpublish first.
  • Changing project_category after uploading changes which documents are mandatory — the child rows keep their old is_mandatory until the DD step is saved again.

7. Location

Fields: location (neighbourhood), address_line_1/2, city, state (county), postal_code, country (Link to Country), latitude, longitude.

Why it matters beyond display:

  • city is a hard requirement for marketplace listing (§14).
  • Changes cascade to all Project Unit rows, which is what the AI search and swipe deck filter on.
  • Kenya counties are also available as Registry Dictionary Concept class Kenya Counties (32 entries).

Completeness: address_line_1 + city + country = 5 points each = the whole 15-point Location category.


8. Media

Two distinct systems — this trips people up.

A. Fields directly on the project (wizard Step 4, update_project_media)
cover_image, project_logo, brochure (Attach), video_url, virtual_tour_url (Data).
Files are uploaded first via Frappe's upload API, then the returned file_url is written to the field.

B. Project Media doctype — the gallery (/manage/media)
One row per asset: media_type (Registry concept: Image, Video, Brochure, Floor Plan, 3D Tour, Document), title, media_file or media_url, is_cover, display_order, is_public. Managed by services/project_media_service.py. Setting a cover here also satisfies the completeness check.

C. Project External Link — child table, up to 15 rows.
Validation: URL must start with https://, title required, ≤100 chars, HTML stripped. Types: Gallery, Video, 3D Tour, Floor Plans, Documents, Other.

Media completeness (10 pts): logo 3 · cover image or a Project Media row with is_cover 3 · ≥3 gallery media rows 2 · any of brochure/video/tour 2.


9. Physical structure — the four-level hierarchy

Real Estate Project
└── Project Phase          (PHS-####)   "Phase 1", "Tower Release A"
    └── Property Container (CNT-####)   a Building / Block / Tower
        └── Property Sub Container (SUB-####)  a Floor / Wing / Section / Block / Cluster
            └── Project Unit (UNIT-####)       the sellable thing

Service: services/project_structure.py

9.1 Project Phase

  • Every project gets "Phase 1" auto-created in after_insert with is_default = 1. Default phases are exempt from the subscription phase limit.
  • Fields: phase_name, phase_number (unique within project), phase_status, phase_budget, start_date, expected_completion_date, actual_completion_date.
  • bulk_create_phases_logic is idempotent and additive — phase numbers that already exist are skipped, not errors. The response reports total_created vs total_skipped.
  • FE caps bulk creation at 20 phases per request.
  • Deletion is blocked if the phase has any buildings. It is not blocked for the default phase.

9.2 Property Container (Building / Block)

  • Fields: container_name, phase (required), container_type (required), total_floors, container_description, building_permit_number, architect, contractor, construction dates.
  • bulk_create_containers_logic:
    • If no phase is supplied it creates/reuses a "Default Phase" with a non-clashing phase number.
    • Names like Building A / Building 5 are treated as disposable placeholders and replaced with a collision-free Building <LETTER> (Excel-style: A…Z, AA, AB…).
    • A user-typed name that already exists in the project is skipped, not an error.
    • If total_floors > 0, floors are auto-created as Floor 1..N using the floor Sub Container Type. A failure here is logged but does not fail the building.
  • FE caps bulk creation at 50 buildings per request.
  • Deletion: blocked if the building has units; otherwise floors are cascade-deleted with it.
  • move_block_to_phase re-parents a building.

9.3 Property Sub Container (Floor)

  • Fields: sub_container_name, container, sub_container_type (floor / wing / section / block / cluster), floor_number.
  • bulk_create_floors_logic skips duplicate floor numbers and reports them.

9.4 Project Unit

The single most field-heavy doctype in the app (~90 fields). Grouped:

Group Fields
Identity unit_number (required, unique per container), container, sub_container, project (read-only), unit_type, unit_type_template, status
Specs built_up_area, carpet_area, balcony_area, bedrooms, bathrooms, parking_spaces, amenities, special_features, description
Pricing base_price, price_per_sqft, currency (default KES), discount_percentage, discounted_price, final_price, pricing_notes
Sales customer, booking_date, reservation_expiry, offer_preparation_expiry, offer_dispatched_at, reservation_accepted, sales_agent, referring_agent, commission_rate, linked_lead, sold_offline
Contract sale_agreement_signed/date/document, last_sale_agreement_request
Payments payment_plan, schedule_activated, schedule_is_custom, schedule_is_customized, total_amount_paid, outstanding_balance, payment_completion_percentage, child tables unit_payments, unit_payment_schedule
Ownership journey stamp_duty_*, lcb_consent_*, transfer_*, title_deed_*, membership_*
Fees transaction_fees child table, fees_frozen, total_transaction_fees, total_cost_of_ownership, total_fees_paid, total_fees_outstanding, fees_payment_complete
Denormalised location project_city, project_county, project_country, project_location, project_latitude, project_longitude

Bulk creation (bulk_create_units_logic):

Aspect Behaviour
Sync vs async quantity ≥ 20 → background job (long queue, 1h timeout), returns HTTP 202 + job_id; poll get_bulk_unit_creation_status. Below 20 it is synchronous and returns the created units.
naming_pattern = floor_based Requires floors to exist, else 400. start_number encodes floor+unit: 220 = start at floor 2, unit 20. Numbers are {prefix}{floor}{unit:02d}A-101, A-102, 201. Max 99 units per floor.
naming_pattern = sequential {prefix}{start_number + i}Villa-1, Villa-2. Does not need floors.
Duplicates Skipped, reported in skipped.
Defaults status = "Available", unit_type = "TBD" when not supplied.
Template If template_id given, unit_type is taken from the template and unit_type_template is stamped on each unit.

Deletion is blocked for units whose status is sold / reserved / booked / fully_paid / partially_paid. Others delete along with their Unit Status Log rows.


10. Unit Type Templates

Doctype: Unit Type Template (UTP-.####) · Service: services/template_management.py

The reusable spec for a kind of unit — "2 Bed Type A". This is the object the public marketplace lists, not individual units.

Fields: template_name (unique per project), unit_type (System Options), project, slug, description, areas, bedrooms/bathrooms/parking_spaces, base_price, price_per_sqft, currency, pricing_notes, amenities, special_features, child table Template Media, flags is_active, is_default, is_listed_on_marketplace.

  • Applying a template to units: bulk_apply_template(unit_ids, template_id) copies specs and pricing onto each unit.
  • Deleting a template is guarded by count_saved_property_references (wishlist rows point at templates) and invalidates the marketplace caches.
  • ⚠️ Frappe recycles UTP- docnames on delete. Anything referencing a template must use a Link field, never a stored string. (See SAVED_PROPERTY_TEMPLATE_IDENTITY_PLAN.md.)

11. Amenities

Table MultiSelect of Project Amenity → Link to System Options category Amenity (34 options: swimming pool, gym, playground, clubhouse, 24/7 security, CCTV, generator, gated community, solar power, smart home, …).

Edited on Step 1 (Basics) in the current wizard, but persisted through the structure endpoint (update_project_structure with amenities as a JSON array). Unknown option names are silently dropped. The list is fully replaced on each save.

The FE groups them into 10 display categories by regex on the display name (Wellness & Recreation, Family & Community, Convenience & Services, Green & Sustainability, Security & Safety, Technology & Connectivity, Parking & Transport, Luxury & Premium, Business & Workspaces, Other) — this is cosmetic only.


12. Financials

12.1 Listing mode — the most consequential setting

Mode Meaning
marketplace (default) QejaVerse generates leads; the developer closes the sale offline. Commission trigger is forced to "On Sale".
full The full digital sales pipeline: reservations, offer letters, e-signature, payment schedules, client portal. Requires Developer Profile.crm_enabled = 1 — otherwise the doctype throws and the API returns a readable 400.

12.2 Agent commission

Field Notes
commission_type "" / Percentage / Fixed Amount
default_commission_percentage 0–100, validated
default_commission_amount ≥ 0, validated
commission_trigger On Sale / On Deposit / On Percentage Paid / On Full Payment / On Handover
commission_trigger_percentage 0–100, only meaningful with "On Percentage Paid"

Marketplace-mode projects always get On Sale written back regardless of what is sent.

12.3 Transaction fees

Child table Project Transaction Fee. Auto-populated at project creation from get_default_kenyan_fees(property_template) — the standard Kenyan set (stamp duty, legal fees, registration, etc.). Editable in the wizard Financials step and at /manage/financials.

Per row: fee_name, fee_code, calculation_type (Fixed Amount / Percentage of Purchase Price), percentage_rate, fixed_amount, applies_to (All Unit Types / Specific Unit Type Template), unit_type_template, journey_step (sale_agreement / stamp_duty / regulatory / transfer / …), is_mandatory, display_order, paid_to_type (Developer / External Party), external_party_name.

Fees are resolved onto a unit (resolve_transaction_fees) and then frozen (freeze_transaction_fees) once reviewed, so later project-level edits do not change an in-flight sale.

12.4 Payment deadlines (client-portal SLAs)

client_response_deadline_days, offer_preparation_deadline_days, developer_acceptance_deadline_days, offer_signing_first_deposit_deadline_days, payment_reminder_days (comma list, e.g. 7,1), payment_grace_period_days. Each falls back to the matching field on the QejaVerse Settings single doctype (defaults 7 / 14 / 2 / 7 / "7,1" / 3).

12.5 Application billing

custom_rate_per_unit — per-project override of the subscription per-unit rate.


13. Payment Plan Templates

Doctype: Payment Plan Template (PPT-{project}-{####}) · Service: services/payment_plan_management.py · UI: /manage/payment-plans

The offer a buyer chooses. Required fields: project, plan_name, payment_plan_type (Registry concept — 23 options: Cash Payment, Bank Mortgage, Construction-Linked, 50/50 Plan, Post-Handover, Rent-to-Own, …), down_payment_percentage, number_of_installments, installment_frequency (Monthly / Quarterly / Semi-Annually / Annually / Milestone-Based).

Optional: payment_method_category (Cash / Mortgage / Flexible / Custom), linked_financier (Financier Master), grace_period_days, late_payment_penalty_percentage, early_payment_discount, is_active, is_default, terms_and_conditions, description.

Child table Payment Milestone: milestone_number, milestone_name, milestone_type (Booking / Construction Stage / Handover / Post-Handover), percentage_of_total, amount, due_date_type (Days from Booking / Specific Date / On Construction Milestone), days_from_booking, specific_date, grace_period_days, description.

Downstream: a plan is activated onto a unit (activate_payment_schedule) which generates the Unit Payment Schedule rows. schedule_is_custom = built from scratch; schedule_is_customized = generated from a template then edited.


14. Document templates (offer letters and sale agreements)

Doctype: Offer Letter Template (OTPL-.####) · UI: /manage/document-templates

Per-project rich-text templates. document_typeOffer Letter, Sales Agreement, Non Disclosure Agreement (System Options category Document Template Type). expiry_days defaults to 14.

Merge placeholders available in the editor:
client_name, client_email, client_id_type, client_id_number, unit_number, unit_type, bedrooms, bathrooms, built_up_area, final_price, currency, project_name, project_location, developer_name, developer_company, developer_signatory_name, developer_signatory_title, developer_reg_number, developer_signature, buyer_full_name, buyer_id_number, buyer_signature, agent_name, payment_plan_name, offer_date, offer_expiry_date, agreement_date, agreement_expiry_date.

signatory_display_config (JSON) controls which lines appear under each signature block — Developer can show Title, Organisation, Company Reg No; Buyer can show ID Number only.

Separately, Agent Contract Templates (CTPL-.####) are developer-wide, not project-scoped, and feed the agent contract flow.


15. Team, roles and access

Doctype: Project Team (autoname: hash). One row = one person on one project.
Fields: project, user, role (Link to System Options category User Role), is_active, designation, is_qeja_preferred, assigned_date, assigned_by.

  • The creator is auto-added as project_manager in the same transaction as project creation. If the Project Manager role option is missing the whole creation fails fast with a 500 (deliberate).

  • Available project roles in System Options: project_manager, project_developer, sales_rep.

  • Permission matrix lives in Project Role Permission (role × permission_key × is_allowed, 40 keys per role), grouped by Project / Units / Buildings / Phases / Team / Financial / Payment Plans / Templates / Agents / Reports. Rows exist for: Owner (everything), Manager and developer (everything except delete_project), sales manager (read + export only), Agent (read-only view of project/units/buildings/phases/pricing/commissions/payment plans/templates).

    ⚠️ The matrix is cosmetic — it enforces nothing. It is served by get_project_role_permissions purely so the frontend knows which tabs and buttons to draw. No API endpoint consults it. There is a complete backend enforcement module at casahub_app/casahub_app/config/project_permissions.py (can_perform_action, is_admin_role, …) but nothing imports it — it is dead code. Server-side authorisation is done by team membership and @guard_project_access, not by project role. Treat the matrix as UI configuration, never as a security boundary.

  • Role name resolution. Team roles are stored as System Options names and normalised in the frontend (normalizeRole in Casahub/src/config/projectPermissions.js) before matrix lookup. project_manager, project developer and project_developerdeveloper; project_ownerowner; sales managersales_manager. Every role in live use resolves to a configured entry except Sales Manager, whose lookup misses (the matrix is keyed sales manager with a space, the lookup asks for sales_manager) and silently falls through to FALLBACK_PERMISSIONS in the same file. That fallback is currently byte-identical to the database, so behaviour is correct — but the admin permission editor has no effect on that role. See G-5.

    viewer was a Project Role option with zero permission rows and no fallback entry, so anyone assigned it saw a management screen with no tabs. Deactivated 2026-07-29 (patch deactivate_viewer_project_role); Agent is the working view-only role. sales_manager and sales manager remain duplicate options — the invite dropdown shows "Sales Manager" twice.

  • Invitations: send_team_invitation(email, project, role, full_name, designation). Existing users are added directly and notified; new users get a registration link (Team Invitation, expiry from QejaVerse Settings invitation_expiry_days, default 7). batch_invite_agents does the same for a list. Invitations can be resent or cancelled.

  • Agent access: require_contract_for_agent_access — when on, an agent only gains project access after signing an agent contract. Agents can also self-request access (Project Access Request) or be invited.

  • Subscription: adding a team member whose role name contains "agent" consumes an agent slot.


16. Compliance (post-construction)

Project-level gates on the Compliance tab, written through update_project_compliance_gate (allow-list: land_rent_clearance, land_rates_clearance, sectional_plan_registered, each with a paired date field). lcb_consent_required is a plain checkbox on the doctype.

These sit above the per-unit ownership-journey gates (stamp duty → LCB consent → transfer lodged → title deed → membership). get_compliance_impact_count reports how many units have paid stamp duty but not yet lodged transfer — i.e. how many buyers are waiting on the developer's clearances.


17. Setup completeness and status tiers

Service: services/project_completeness.py

Two calculation modes. If any Project Setup Checklist rows exist for the project it uses those (weighted per category); otherwise — the normal case — it uses automatic field-based scoring:

Category Weight How the points are earned
Basics 20 category 3 · development type 3 · description 3 · total budget 3 · land area 3 · start date 2.5 · completion date 2.5
Location 15 address_line_1 5 · city 5 · country 5
Media 10 logo 3 · cover 3 · ≥3 gallery items 2 · brochure/video/tour 2
Structure 20 ≥1 phase 6 · a phase with budget > 0 4 · ≥1 building 6 · a building with floors > 0 4
Units 20 ≥1 unit 10 · ≥5 units 5 · ≥1 unit priced 5
Due Diligence 15 all mandatory uploaded 10 · ≥1 optional uploaded 3 · nothing expired 2

Status tiers: 0–39 Draft · 40–69 Basic Setup · 70–89 Structured · 90–99 Ready for Review · 100 Published.

update_project_completeness is called after every wizard save and after every structural change (~30 call sites, including doctype hooks on Project Unit, Project Phase, Property Container, Project Media and Project Team).

Scoring never publishes. The tier label reads "Published" at 100%, but that is the completeness tier, not the is_published flag — the two are independent. Publishing only ever happens through publish_project_logic (§18). This used to be otherwise; see G-2.


18. Publishing

publish_project_logic — invoked from Step 8 (Review). Gates, in order:

  1. Project exists and is not already published.
  2. Caller is System Manager or created_by or an active Project Team member.
  3. All mandatory due-diligence documents have files — else 400 listing the missing ones.
  4. Completeness ≥ QejaVerse Settings.minimum_publish_completeness (default 70%).
  5. If listing_mode = "marketplace":
    • listing_confirmed_by_developer must be ticked (the accuracy attestation on the Review step, stamped with user + timestamp),
    • the project must have a developer_profile,
    • that developer must have accepted the current version of the Developer Platform Agreement (commission terms, clause 4.4).
  6. company must be set.

On success: is_published = 1, published_date = now(), project_status set to the active option if it was Draft, and (marketplace, first publish only) a "now live on QejaVerse" welcome email is sent to the developer.

The UI adds one more gate the backend does not have: the Review step disables Publish unless the Developer Profile itself is published (isDeveloperPublished === true).

unpublish_project_logic reverses it with the same permission check. Unpublished projects remain visible to team members only.


19. Marketplace listing — a separate, per-unit-type gate

Publishing makes a project visible to agents. It does not put it on the public marketplace. Each Unit Type Template is listed individually via toggle_template_marketplace_listing, and listing ON runs a 7-point readiness check (get_template_marketplace_readiness_logic):

  1. Project is published
  2. Developer profile is published
  3. Project has a city
  4. Template is_active
  5. Template base_price > 0
  6. Template name is set
  7. ≥1 available unit linked to the template

Delisting always succeeds. The public listing query additionally enforces all of the above at read time, and results are cached 5 minutes (home_listings:*, invalidated by cache_invalidation.py).


20. Project onboarding (units already sold before joining)

Doctype: Project Onboarding Record (autoname = project name, so one per project) · Service: services/onboarding_service.py · UI: /manager/projects/:project/onboarding

Purpose: a developer joining QejaVerse mid-project already has sold units. They declare how many, attest to it legally, then run a per-unit wizard to seed each sale.

  • initialize_project_onboarding — idempotent; rejects a declared count above total_units; lock_period_days comes from the developer profile (default 90).
  • confirm_attestation — stores generated attestation text, typed name, user, IP, timestamp. Immutable once confirmed.
  • Seeded units carry onboarding_seeded = 1, which suppresses scheduler-driven automation (payment reminders, reservation expiry, e-sign expiry, action items) so historical data does not generate live notifications.
  • Units unlock/lock over the lock period; there are pre-lock notifications and an early-lock action.

21. The 8-step setup wizard

Route: /projects/setup/new/projects/setup/:projectId · Component Casahub/src/projects/ProjectSetupWizard.jsx

Step Name Writes via Notes
1 Basics create_project_draft then update_project_basics; amenities via update_project_structure; SPV via set_project_company The only step that creates the project. Auto-saves on blur once the project exists. Duplicate-name check on blur.
2 Due Diligence save_due_diligence_documents (files uploaded first) Requires a project_category to show anything.
3 Location update_project_location
4 Media update_project_media Uploads cover/logo/brochure then saves URLs.
5 Structure update_project_structure + bulk_create_phases / bulk_create_containers / bulk_create_floors Auto-creates Phase 1..N if number_of_phases exceeds existing count. Phase-creation failure does not block navigation.
6 Units bulk_create_units_from_template, create_unit
7 Financials update_project_financials Commission + payment settings + transaction fees in one call.
8 Review confirm_project_listing, publish_project Shows completeness breakdown and the publish gates.

The wizard is non-linear — the stepper lets you jump to any step. Forward navigation happens through each form's "Save & Continue"; the footer only offers Previous (and ← keyboard).

Team management is not in the wizard. It lives in post-setup management.


22. Post-setup management

Route /projects/:projectId/manage/<tab> · Casahub/src/projects/manage/ManageLayout.jsx

Tabs, each visibility-gated by a Project Role Permission key: Units · Buildings · Phases · Media · External Links · Compliance · Due Diligence · Document Templates · Templates · Payment Plans · Financials · Company · Team · Settings.


23. Known gaps and traps

These are real, reproducible behaviours found while writing this document. Treat them as expected-current-behaviour when testing, and as a backlog.

ID Area Finding Impact
G-1 Due diligence FIXED 2026-07-29. Due Diligence Category Rule rows existed only for Residential and Commercial, so Hospitality, Industrial and Mixed-Use projects required zero documents. Mixed-Use rules are now seeded (patch seed_mixed_use_due_diligence_rules); Hospitality and Industrial are no longer selectable as project categories. Was: such a project could publish with no legal documentation and its DD score was permanently 0.
G-2 Publishing FIXED 2026-07-29. update_project_completeness auto-set is_published = 1 at 100% + mandatory docs, bypassing publish_project_logic's gates (listing-accuracy attestation, platform-agreement check, permission check). The auto-publish block is removed; publishing is now only ever explicit. Guarded by tests/test_project_completeness.py. Was: a marketplace project could go live without the developer's accuracy attestation.
G-3 Publishing The Review UI blocks publish unless the Developer Profile is published; the backend publish_project_logic never checks this. API callers (or G-2) can publish a project belonging to an unpublished developer. Its unit types still can't reach the marketplace (§19 check 2), so the effect is a half-live project.
G-4 System Options data create_default_phase looks for Phase Status option_value = "planning" and publish_project_logic looks for Project Status option_value = "active"neither value exists in System Options (phase statuses are cancelled/on_hold/in_progress; project statuses are Planning/PreLaunch/Launched/Construction/Ready/Completed). Auto-created phases have no status; published projects keep an empty project_status.
G-5 Permissions Partly FIXED 2026-07-29, and the original description was wrong — see §15 for what is actually true. The permission matrix is cosmetic: it only decides which tabs and buttons the frontend draws, nothing on the server consults it. normalizeRole correctly aliases project_manager/project_developerdeveloper, so most roles resolve fine. Two real defects: (a) viewer had no permission rows at all → empty management screen (now deactivated, patch deactivate_viewer_project_role); (b) sales_manager and sales manager are duplicate System Options, and the frontend's underscore lookup misses the space-keyed database rows, so Sales Manager silently uses a hardcoded copy in projectPermissions.js. (a) was live for 2 users. (b) is inert today — the hardcoded copy is byte-identical to the database — but edits made in the permission admin screen have no effect for that role, and the invite dropdown shows "Sales Manager" twice.
G-6 Wizard Step 5 ProjectStructureForm sends container_type to update_project_structure, whose signature does not accept it — Frappe drops unknown kwargs silently. The "Building Type" chosen on the Structure step is never saved to the project record (it is still used for the buildings created in that session).
G-7 System Options hygiene Heavy duplication: Unit Type has 1Br/1 bedroom, 4 bedroom/4br, VillaBungalow/villa bungalow; Unit Status has sold + FullyPaid + fully paid; Container Type has TownhouseRow/townhouse row/Town House. Dropdowns show near-identical options; filtering and reporting split across synonyms.
G-8 Phases Deleting the default phase is not blocked. A project can end up with zero phases, which zeroes the Structure score and breaks unit rollups.
G-9 Bulk units bulk_create_units_logic writes a full frappe.log_error "[DEBUG] Subscription Check" entry on every call. Error-log noise in production.
G-10 Existing data Two projects in the current DB are is_published = 1 at 11% completeness / Draft status (PRJ-0475, PRJ-0470). Likely fixtures written directly; confirms publish state can diverge from completeness.

24. File map (where to look)

Concern Path
Project doctype + controller casahub_app/casahub_app/doctype/real_estate_project/
Create / update / publish casahub_app/casahub_app/services/project_setup.py
Completeness scoring casahub_app/casahub_app/services/project_completeness.py
Phases / buildings / floors / bulk units casahub_app/casahub_app/services/project_structure.py
Unit CRUD, bulk edit, apply template casahub_app/casahub_app/services/unit_management.py
Unit type templates casahub_app/casahub_app/services/template_management.py
Due diligence casahub_app/casahub_app/services/due_diligence_service.py
Transaction fees casahub_app/casahub_app/services/transaction_fee_service.py
Payment plans casahub_app/casahub_app/services/payment_plan_management.py
Team / access requests / invitations casahub_app/casahub_app/services/project_access.py, utils/invitation_utils.py
Marketplace listing + public queries casahub_app/casahub_app/services/discover_service.py
Onboarding of already-sold units casahub_app/casahub_app/services/onboarding_service.py
Subscription limits + hooks casahub_app/casahub_app/services/subscription_service.py, subscription_hooks.py
All whitelisted endpoints casahub_app/casahub_app/api.py
Doc events wiring casahub_app/hooks.py
Wizard Casahub/src/projects/ProjectSetupWizard.jsx + Casahub/src/projects/forms/
Management tabs Casahub/src/projects/manage/
FE API layer Casahub/src/services/projectSetup.js, projects.js, templates.js, paymentPlans.js
Last updated 2 hours ago
Was this helpful?
Thanks!