Offers: saving an offer unchanged writes a revision, reopens a ticked-off reminder and undoes a postponement #442
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#442
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-09-29 in a read-only review of postulo at
95348908d, reading the offer views and services and checking the result with a unit test against the snapshot.Checked again on 2026-10-01 against
mainat0a5cad587: still present. The code is now atapplications/views.py:1355-1361(OfferUpdateView.form_valid);applications/services.pyhas not moved, and the same unit test gives the same results.Every save of the offer form is treated as a revision of the offer and a new answer-by date, whatever changed. Adding a line to Notes writes "Offer revised" on the timeline, sends an
offer_recordedwebhook withrevised=true, and rebuilds the answer-by reminder: one the person ticked off is outstanding again, one they put off is moved back, and one they deleted is made afresh.Where:
applications/views.py:1315-1321(OfferUpdateView.form_valid),applications/services.py:704-714(revise_offer),applications/services.py:645-679(_remind_to_answer)What happens:
OfferUpdateView.form_validrunsoffer = form.save()and thenrevise_offer(offer)with no look atform.has_changed()orform.changed_data.revise_offeralways appends anEventKind.OFFERentry, "Offer revised: …", and always calls_remind_to_answer._remind_to_answerknows nothing of the previousanswer_by. For a date still ahead it rewrites the summary, setsdone_at = Noneon a reminder that was done, and callspostpone_reminder(reminder, due), which putsdue_atback to 09:00 the day before and clearsnotified_at. WhenOffer.reminderis null (the reminder was deleted; the field isSET_NULL), it creates a new one.Checked: an offer with
answer_byten days out, reminder ticked off, then a notes-only edit posted throughapplications:offer_update. The timeline went from three entries to four ("Offer revised: 65,000 EUR per year") and the reminder'sdone_atwasNoneagain. A reminder put off to a later time moved from2026-10-05 23:01back to2026-10-09 07:00UTC after another notes-only edit, and after deleting the reminder a further edit made a new one.Why it matters: the timeline is the record of what happened, and it now says the terms changed when they did not. A reminder the person dealt with or deliberately moved comes back and is announced through their notifiers, and a webhook consumer is told of a revision that never happened.
change_status(applications/services.py:101) already refuses to "litter the timeline with entries saying nothing changed"; the offer path does not.Fix: in
OfferUpdateView.form_valid, return without callingrevise_offerwhennot form.has_changed(). Haverevise_offertake the fields that changed (or the previous row) and write the timeline entry and webhook only when the terms changed (base_amount,currency,period,variable_pay,equity,benefits,location,holidays,starts_on,answer_by), not fornotesalone. Pass the previousanswer_byto_remind_to_answerand touch the reminder only when the date moved; never reopen a ticked-off reminder or recreate a deleted one unless it did.Done when
tests/test_offers.pyhas a test that edits only the notes of an offer whose reminder was ticked off and asserts no newEventKind.OFFERentry,done_atunchanged; a second that postpones the reminder, edits the notes and assertsdue_atunchanged; and a third that movesanswer_byand still sees the reminder follow it.