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_events → on_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 aQejaverse Companywithcompany_type = "Project"andparent_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_CATEGORIESin
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_TYPESfirst, 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_logicreplaces the whole child table on every save. It re-derivesis_mandatoryfrom the category rules — a client cannot forge it.- A row whose
expiry_dateis in the past is stampedstatus = "Expired"on save. due_diligence_statusrollup: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_categoryafter uploading changes which documents are mandatory — the child rows keep their oldis_mandatoryuntil 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:
cityis a hard requirement for marketplace listing (§14).- Changes cascade to all
Project Unitrows, which is what the AI search and swipe deck filter on. - Kenya counties are also available as
Registry Dictionary Conceptclass 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_insertwithis_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_logicis idempotent and additive — phase numbers that already exist are skipped, not errors. The response reportstotal_createdvstotal_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 5are treated as disposable placeholders and replaced with a collision-freeBuilding <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 asFloor 1..Nusing thefloorSub 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_phasere-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_logicskips 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. (SeeSAVED_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_type ∈ Offer 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_managerin 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 exceptdelete_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_permissionspurely so the frontend knows which tabs and buttons to draw. No API endpoint consults it. There is a complete backend enforcement module atcasahub_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 Optionsnames and normalised in the frontend (normalizeRoleinCasahub/src/config/projectPermissions.js) before matrix lookup.project_manager,project developerandproject_developer→developer;project_owner→owner;sales manager→sales_manager. Every role in live use resolves to a configured entry except Sales Manager, whose lookup misses (the matrix is keyedsales managerwith a space, the lookup asks forsales_manager) and silently falls through toFALLBACK_PERMISSIONSin 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.viewerwas aProject Roleoption with zero permission rows and no fallback entry, so anyone assigned it saw a management screen with no tabs. Deactivated 2026-07-29 (patchdeactivate_viewer_project_role); Agent is the working view-only role.sales_managerandsales managerremain 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 Settingsinvitation_expiry_days, default 7).batch_invite_agentsdoes 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:
- Project exists and is not already published.
- Caller is System Manager or
created_byor an activeProject Teammember. - All mandatory due-diligence documents have files — else 400 listing the missing ones.
- Completeness ≥
QejaVerse Settings.minimum_publish_completeness(default 70%). - If
listing_mode = "marketplace":listing_confirmed_by_developermust 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).
companymust 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):
- Project is published
- Developer profile is published
- Project has a city
- Template
is_active - Template
base_price > 0 - Template name is set
- ≥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 abovetotal_units;lock_period_dayscomes 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 |
|---|---|---|---|
| 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. | |
| 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_developer → developer, 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 |