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
25 days ago
Linear
Get notified by email when there are changes.
Completed
Main App
25 days ago
Linear
Get notified by email when there are changes.