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.
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)
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->salesInstructionlooks up viaproperty_id + owner_id - Fees are stored on
SalesInstruction(implementsFeeable) - Sale stores only calculated totals:
total_upfront_fees_pence, etc.
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 |
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 differentCode 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->salesApplicantaccesses - 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->salesApplicantaccess needs review - PDF/memo generation needs significant rework
- Risk of regression in existing offer→sale flow
Effort Estimate: Medium-High
Create a new model alongside Sale.
Changes Required:
- New
third_party_salestable andThirdPartySalemodel - 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
Given the detailed analysis showing ~40 files would need changes for Option A, Option B (Separate Entity) is recommended:
Why Option B:
- Zero regression risk - No changes to 40+ existing files
- 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
- Cleaner mental model - Different entities for different workflows
- Instruction linking is straightforward - Same pattern as regular Sale
Option B Implementation:
- New
third_party_salestable - New
ThirdPartySalemodel 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)
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_idFK - 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 offersstc(exchanged) - Sold subject to contractcompleted- Sale completedfallen_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
| 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 |
These can be addressed during implementation:
- Should third-party sales trigger the same webhooks as regular sales?
- Do we need document generation (e.g., completion statements)?
- Can agents convert a third-party sale to a regular sale if the buyer comes through Street?
When creating/updating a third-party sale:
- Check no other active sale (regular or third-party) exists for the property
- Update property status to match third-party sale status
- On completion, update property status to 'sold'
- On fall-through, revert property status if no other active sales
For the property sales history and reports:
- Query both
salesandthird_party_salestables - Union results with a
sale_typediscriminator - Apply 'third-party' badge/indicator in UI
- Property Status Conflicts: Need to ensure a property can't have both an active regular sale and active third-party sale simultaneously
- Reporting Complexity: Combining two tables for reports may require union queries
- Feature Parity: Third-party sales are intentionally simpler - resist scope creep to match all Sale features
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