Skip to content

Instantly share code, notes, and snippets.

@IdleMuse
Last active January 12, 2026 16:41
Show Gist options
  • Select an option

  • Save IdleMuse/371fd5223991787d8ba3bb488ca2c000 to your computer and use it in GitHub Desktop.

Select an option

Save IdleMuse/371fd5223991787d8ba3bb488ca2c000 to your computer and use it in GitHub Desktop.
Technical Discovery: Third-Party Sales for Street CRM

Technical Discovery: Third-Party Sales

Overview

Agents sometimes receive fees for sales handled by third parties (e.g., auctions, other agents). Currently there's no way to track these in Street without going through the standard offer → sale flow, which requires an applicant.

This document outlines a proposed solution for tracking these "third-party sales" separately.

User access: Created from the property page when recording a sale handled externally.

Requirements Summary

From discovery:

  • Price: Sale price if known
  • Date: Sale/completion date
  • Fee: The fee the agent is due (may differ from instruction fee)
  • Third-party company: Who is handling the sale (optional)
  • Statuses: Same as regular sales (under offer, SSTC, completed, withdrawn)
  • Actions: Mark as SSTC, Complete, Fall Through
  • Applicant: Can link if known, but usually won't be (and that's fine)
  • Instruction: Link via property + owner (same as regular Sale)

Current Architecture Analysis

Sale Model (src/Domain/SalesAndLettings/Sales/Models/Sale.php)

Field Requirements:

Field Currently Required Notes
sales_applicant_id Required 40+ places assume non-null
sales_offer_id Nullable Already optional
property_id Required
owner_id Required
sales_listing_id Nullable

Instruction Relationship (important):

  • Sale does NOT have a direct FK to SalesInstruction
  • Uses computed accessor: $sale->salesInstruction looks up via property_id + owner_id
  • Fees are stored on SalesInstruction (implements Feeable)
  • Sale stores only calculated totals: total_upfront_fees_pence, etc.

Code Coupling Analysis

If sales_applicant_id becomes nullable, these break:

Location Impact
SalesService.php:68-69 Direct chained property access
SaleCompletionService.php:157 Repository lookup with null ID
MemorandumOfSaleService.php (6+ locations) PDF generation completely fails
SaleTransformer.php:47-49 Passes null to transformer
Blade templates (40+ locations) Direct $sale->salesApplicant-> access
Vue components Assumes salesApplicant.data exists

Approach Comparison

Option A: Modify Existing Sale Model

Make sales_applicant_id nullable, add new fields for third-party info.

Changes Required:

// New fields on Sale
$table->unsignedBigInteger('sales_applicant_id')->nullable()->change();
$table->string('third_party_company')->nullable();  // Who's handling sale
$table->boolean('is_third_party')->default(false);  // Discriminator
$table->integer('override_fee_pence')->nullable();  // Custom fee if different

Code Changes Required (~40 files):

Category Files Impact
Services 6 files MemorandumOfSaleService, SalesProgressionService, SolicitorsService, SalesChainService, DeletePersonEntityService, SalesDataMapper
Controllers 4 files SalesFallThroughsController, MarkSaleAsCompletedController, SalesNotesController
Charts/Reports 4 files ExchangedSalesReportChart, SalesAgreedReportChart, SalesPipelineReportChart, CompletedSalesReportChart
DataTables 2 files SaleDataTableTrait (10+ locations), SalesEntriesDataTableHelper
Transformers 2 files SaleTransformer (InternalApi & OpenApi)
Other 5+ files Commands, traits, DTOs

Blade Templates (~15 files):

  • send-memo-modal.blade.php (5 locations)
  • notes.blade.php (10 locations)
  • salesProgression.blade.php (3 locations)
  • memorandum-of-sale/master.blade.php (6 locations)
  • property-sale.blade.php, property-pending-offers-cards.blade.php, etc.

Vue Components (2-3 files):

  • SalesHistoryCard.vue (2 locations - main risk)

Additional work:

  • Add null checks to all $sale->salesApplicant accesses
  • Update MemorandumOfSaleService - skip PDF for third-party sales or handle gracefully
  • Add new service method createThirdPartySale() bypassing offer flow

Pros:

  • Single table for all sales
  • Existing reporting/queries work (just need filters)
  • Sale-related features (notes, documents, activity, follow-ups) work automatically
  • Property status integration already exists

Cons:

  • High risk - touching 40+ files
  • Every $sale->salesApplicant access needs review
  • PDF/memo generation needs significant rework
  • Risk of regression in existing offer→sale flow

Effort Estimate: Medium-High


Option B: Separate ThirdPartySale Entity

Create a new model alongside Sale.

Changes Required:

  • New third_party_sales table and ThirdPartySale model
  • New service, controller, events
  • Add relationship to Property: $property->thirdPartySales()
  • Combined queries for reporting

Pros:

  • Zero risk to existing Sale code
  • Clean, simple model tailored to use case
  • Can iterate independently
  • Lower initial effort

Cons:

  • Two places to look for sales
  • Union queries for combined reports
  • Need to duplicate some functionality (notes, activity, status updates)
  • Property status integration needs new implementation

Effort Estimate: Medium


Recommendation

Given the detailed analysis showing ~40 files would need changes for Option A, Option B (Separate Entity) is recommended:

Why Option B:

  1. Zero regression risk - No changes to 40+ existing files
  2. Third-party sales don't need most Sale features:
    • No memo of sale PDF
    • No sales progression events
    • No solicitor tracking
    • No sales chain management
  3. Cleaner mental model - Different entities for different workflows
  4. Instruction linking is straightforward - Same pattern as regular Sale

Option B Implementation:

  • New third_party_sales table
  • New ThirdPartySale model with minimal fields
  • Simpler service (no progression, no solicitors)
  • Combined reporting via union query or abstraction

Trade-offs accepted:

  • Two tables instead of one
  • Union queries for combined sales lists
  • Some code duplication (property status updates)

Key Design Decisions

1. Instruction Linking

  • Inferred via property_id + owner_id (same as regular Sale)
  • No direct FK needed - use same accessor pattern as Sale model
  • Fee defaults from instruction, but can store override if fee differs

2. Optional Applicant

  • Nullable sales_applicant_id FK
  • Can link if buyer is known in Street
  • If no applicant linked, buyer is simply unknown (acceptable for third-party sales)

3. Statuses Same as regular sales for consistency:

  • under_offer - Third party has property under offer
  • sstc (exchanged) - Sold subject to contract
  • completed - Sale completed
  • fallen_through - Sale fell through

4. Property Status Integration

  • Third-party sales update property status like regular sales
  • Check: no other active sale (regular OR third-party) exists
  • On completion: update property status to 'sold'
  • On fall-through: revert property status

Confirmed Decisions

Decision Answer Rationale
Reporting Combined view Third-party sales appear alongside regular sales with a 'third-party' indicator
Invoicing TBD (incremental) Track the fee for now; invoicing can be added later if needed
Property Status Yes, update status Third-party sales change property status (under offer, SSTC, sold) like regular sales

Open Questions (Lower Priority)

These can be addressed during implementation:

  1. Should third-party sales trigger the same webhooks as regular sales?
  2. Do we need document generation (e.g., completion statements)?
  3. Can agents convert a third-party sale to a regular sale if the buyer comes through Street?

Behavioural Summary

Property Status Integration

When creating/updating a third-party sale:

  1. Check no other active sale (regular or third-party) exists for the property
  2. Update property status to match third-party sale status
  3. On completion, update property status to 'sold'
  4. On fall-through, revert property status if no other active sales

Combined Sales View

For the property sales history and reports:

  • Query both sales and third_party_sales tables
  • Union results with a sale_type discriminator
  • Apply 'third-party' badge/indicator in UI

Risks & Considerations

  1. Property Status Conflicts: Need to ensure a property can't have both an active regular sale and active third-party sale simultaneously
  2. Reporting Complexity: Combining two tables for reports may require union queries
  3. Feature Parity: Third-party sales are intentionally simpler - resist scope creep to match all Sale features

Summary

Recommended: Option B (Separate ThirdPartySale Entity)

This approach:

  • Zero risk to existing 40+ files that access $sale->salesApplicant
  • Links to instruction via same pattern as regular Sale (property + owner lookup)
  • Optionally links to SalesApplicant if buyer is known
  • Clean separation between standard and third-party workflows
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment