The builder does not change when the house sells
Whether a garage door call is warranty or billable depends on who bought the door, not who owns the house. One extra lookup on Location is what makes that answerable — and one custom warranty date field that had to be deleted the next day.
A technician arrives at a house with a garage door that will not close. Before anything else, dispatch needs to know one thing: is this a warranty call or a billable one?
At Doorvana that question has a non-obvious answer, and it drove a data model decision I want to write down because I think it generalises to every home services company that touches new construction.
The situation the model has to survive
A builder puts up a house and buys the garage door. Eighteen months later the house sells. The homeowner calls about the door.
Who is the customer? The homeowner — they own the house, they made the call, they are who the technician meets. Who bought the door? The builder. Whose warranty applies? The builder’s purchase.
If the Location carries a single Account lookup, the moment the house sells, one of those two facts gets overwritten. Point it at the homeowner and the warranty trail is gone. Leave it on the builder and you cannot bill anybody.
Two lookups, not one
force-app/main/default/objects/Location/fields/Builder_Account__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Builder_Account__c</fullName>
<label>Builder Account</label>
<description>The builder that originally bought work at this property.
Deliberately separate from the property's current owner — the builder
does not change when the house sells, and that separation is what makes
a warranty-versus-billable decision possible for a homeowner living in a
builder-supplied home.</description>
<type>Lookup</type>
<referenceTo>Account</referenceTo>
<relationshipLabel>Builder Properties</relationshipLabel>
<relationshipName>Builder_Locations</relationshipName>
<deleteConstraint>SetNull</deleteConstraint>
</CustomField> One extra lookup. That is the whole fix, and it is worth the field because the alternative is a question nobody can answer at the moment they need it.
Warranty dates are not a formula, and they are not a field either
The tempting next move is Warranty_End_Date__c = InstallDate + 365. New construction
warranties are a year, the data is already there, and a formula field costs nothing to
maintain.
It is wrong the moment you look at what is actually installed.
A new construction warranty might be a year. The door manufacturer’s warranty on panels and springs runs on its own terms. The operator manufacturer warrants the motor on different terms again, often longer. Parts carry their own. A formula returns a confident, precise, wrong answer for every item that is not the default case — and a wrong warranty date does not fail loudly. It gets quoted to a customer.
So I made Asset.Warranty_End_Date__c a plain Date rather than a formula. Someone or something
sets it. More work at data entry, honest about the fact that the answer varies. I wrote that
down in the build note, including the phrase “no shadowing”, and moved on.
It was shadowing. Not a field — an entire object model I had not described.
WarrantyTerm and AssetWarranty already exist
The next day’s work was a diagnosis checklist for a home-warranty network, which wanted parts
and labor guarantee durations on every diagnosis. Before writing any fields I described
WorkOrderLineItem, WorkOrder, Asset, ProductConsumed, AssetWarranty and WarrantyTerm
against the org. Two of those I had never opened.
WarrantyTerm defines a coverage term. Parts, labor and expenses each get their own
duration and their own unit of time — a term is not one number. AssetWarranty applies a term
to an individual Asset and stamps the resulting end dates separately:
the standard warranty chain, queried
SELECT Id, AssetId, WarrantyTermId,
PartsCoveredEndDate, LaborCoveredEndDate, ExpensesCoveredEndDate,
WarrantyTerm.WarrantyDuration, WarrantyTerm.WarrantyUnitOfTime
FROM AssetWarranty
WHERE AssetId = :assetId And one Asset can carry several AssetWarranty records. Which is precisely the problem I had
written two paragraphs arguing was real — manufacturer and part warranties differ by brand and
by component — and then answered with a single date field that cannot express any of it.
Warranty_End_Date__c did not shadow a field on Asset. Nothing on Asset is called that.
It shadowed AssetWarranty, an object, reachable from WorkOrderLineItem through the standard
AssetWarrantyId lookup that Field Service already reads.
The field was removed the same day it was found: a destructiveChangesPre.xml deployment,
validated first with RunLocalTests (10 of 10 passing), then executed against the sandbox. It
had never reached production, so nothing was lost except the build note that claimed it was
clean. Removing a custom field is cheap exactly once — before real data exists in it. That
window is measured in days.
The door and the opener are different products
Door_Brand__c and Operator_Brand__c are two restricted picklists, not one.
This sounds like a detail until you picture the opening: a door from one manufacturer, an opener from another, bolted to the same header. That is not an edge case, it is routine. A single Brand field cannot describe the installation, so parts lookup goes wrong, warranty tracking goes wrong, and the technician ends up phoning the office.
Restricted, both of them — an unrestricted brand picklist accumulates “LiftMaster”, “Lift Master” and “liftmaster” within a quarter, and then no report on brand means anything.
Door_Material__c is a third, for the same reason: steel, wood, composite and glass behave
differently enough that a technician wants it on the record before arriving.
Check the standard fields first
Before adding any of this I listed what Asset already ships with:
InstallDate · PurchaseDate · UsageEndDate · SerialNumber · Status ·
ManufactureDate · StockKeepingUnit
All present, all standard, no custom duplicates. This is a five-minute check that prevents a
specific, expensive mistake: creating Install_Date__c next to a standard InstallDate
splits the data across two fields, and anything reading the standard one — including parts of
Field Service — quietly gets nothing.
It is also the check that passed while Warranty_End_Date__c was being added, because
Asset has no standard warranty end date for it to sit next to. The field list is worth
reading and it is not sufficient on its own.
One field did survive the check honestly. Asset has no standard model number, and
ProductCode only exists when the Asset is tied to a Product2 — which a door somebody else
installed never is:
force-app/main/default/objects/Asset/fields/Model_Number__c.field-meta.xml
<?xml version="1.0" encoding="UTF-8"?>
<CustomField xmlns="http://soap.sforce.com/2006/04/metadata">
<fullName>Model_Number__c</fullName>
<label>Model Number</label>
<description>The manufacturer's model number as read off the door or operator.
Asset has no standard model field, and ProductCode only exists when the Asset
is tied to a Product2; most doors serviced here were installed by someone else
and have no Product2.</description>
<inlineHelpText>Model number from the door or operator label.</inlineHelpText>
<type>Text</type>
<length>80</length>
</CustomField> The Location type nobody had turned on
One more, from the same pass, and it is a good example of an assumption worth testing.
LocationType on the standard Location object had exactly one active value: Virtual. A
physical property could not be typed as a site, because the value did not exist.
Added: Site (default), Warehouse, Plant, Van.
Warehouse is not filler. Doorvana holds no inventory — technicians collect product from vendor locations on the way to a job. Those pickup points are real places that appear in a technician’s day and belong in the system as Location records. A model that only knows about customer sites cannot describe the actual route.
What this buys at the dispatch board
The house has a Location. The Location knows its current owner and, separately, the builder
who bought the original work. The door and opener are Assets on that Location with their own
brands, and their coverage hangs off AssetWarranty records with PartsCoveredEndDate and
LaborCoveredEndDate set from whichever WarrantyTerm applies — one per component, because
the door and the operator were never warranted on the same terms.
So when the call comes in, “is this warranty or billable” is a lookup rather than a phone call to whoever remembers the subdivision. That is the return on one extra lookup field, a decision not to write a formula, and a custom field deleted the day after it was built.