When two Attik inspections are created for the same property/day (as observed in ATT-1475), Priority Lab dedupes them into a single appointment, both Attik inspections persist the same _priorityLabPropertyId, cancelling either one removes the PL appointment for the other, and manual resync silently does not recreate it.
While in a different order I observed the following behavior: Created order online > did not refresh Attik > scheduled inspection internally for same day/time as online > PL appointment did not duplicate > when I cancelled one of the two orders it cancelled the PL appointment > resyncing the original inspection did not resend the appointment to PL
attik-backend/src/util/functions/priorityLab/buildPriorityLabPayload.ts lines 6-17 send inspectionDate as date-only (no time of day):
function formatInspectionDateLocal(datetime: Date, timeZone: string): string {
const fmt = new Intl.DateTimeFormat('en-US', {
timeZone, month: 'numeric', day: 'numeric', year: 'numeric',
});
// ...
return `${Number(m)}/${Number(d)}/${y}`;
}
The full payload (lines 115-146) is:
const payload: CreateUpdateAppointmentPayload = {
firstName, lastName, phoneNumber, address, city, state, zipcode,
inspectionDate: formatInspectionDateLocal(new Date(inspection.datetime), timeZone),
numEmails: String(emailEntries.length),
};
if (propertyId) { payload.propertyId = propertyId; }
No Attik inspection id, slug, or other unique key. Two inspections same property/day produce identical create payloads. PL's CreateUpdateAppointment matches and returns the same confirmation (PL's property id) for both β which Attik then writes to both inspections' _priorityLabPropertyId.
attik-backend/src/util/functions/priorityLab/syncPriorityLabAppointment.ts lines 49-96 (delete branch) calls deleteAppointment with the PL property id and $unsets the field on the cancelled inspection only. The surviving inspection still references the (now-deleted) PL property id.
Two compounding reasons:
(a) BullMQ jobId is fixed per inspection and removeOnFail: false.
attik-backend/src/events/bullmq/priorityLabQueues.ts lines 32-37:
const jobIdForInspection = (inspectionId: number) => `prioritylab-${inspectionId}`;
export async function queuePriorityLabJob(data: PriorityLabSyncJob) {
return priorityLabAppointmentQueue.add('priorityLab', data, {
jobId: jobIdForInspection(data.inspectionId),
});
}
Lines 9-14:
const defaultJobOptions = {
attempts: 5,
backoff: { type: 'exponential' as const, delay: 30000 },
removeOnComplete: true,
removeOnFail: false,
};
A previously-failed job for prioritylab-${inspectionId} can sit in Redis and block new adds for that key, which looks like "resync did nothing".
(b) The 442-recovery branch only recreates when propertyId was set going in.
attik-backend/src/util/functions/priorityLab/syncPriorityLabAppointment.ts lines 210-217:
if (e instanceof PriorityLabError && e.code === '442' && propertyId) {
await Inspection.findByIdAndUpdate(inspectionId, { $unset: { _priorityLabPropertyId: 1 } });
propertyId = undefined;
const { propertyId: newId } = await tryUpsert(undefined);
propIdOut = newId;
}
A non-442 error (or a success that returns the same stale id) leaves the surviving inspection still pointing at a dead PL record.
inspectionId) in the PL payload, or include enough disambiguators (time of day, slug) that PL doesn't dedupe across two Attik rows. Confirm with PL whether they support a per-call external reference._priorityLabPropertyId, verify the returned id isn't already used by another inspection in the same company. If it is, treat it as a collision and recreate without propertyId.defaultJobOptions: change removeOnFail: false to e.g. removeOnFail: { count: 50 } so a stuck failed job doesn't permanently block new adds for the same prioritylab-{id} key.priorityLab.lastError and skip-reasons clearly in the inspection UI so silent no-ops are visible._priorityLabPropertyId value.prioritylab-{id} jobs still occupying the queue for the surviving inspection.CreateUpdateAppointment accepts and respects a per-call external reference id (Attik's inspectionId).Please authenticate to join the conversation.
Completed
Main App
2 months ago
Linear
Get notified by email when there are changes.
Completed
Main App
2 months ago
Linear
Get notified by email when there are changes.