Problem/Motivation
Split out from #3560177: Safer admin overrides.
You are right, there are really two different processes here:
- A cancellation brings all current declarations to an end; normally the start_date would be the same as received_date (earlier is not allowed), and the end_date empty. It is represented by a Cancellation entity.
- A invalidation is a negative response to a confirmation; it makes a single declaration invalid as if it had never been made. There is no opportunity to specify any dates (HMRC says declarations are invariant); it's only allowed within 30 days of a confirmation.
Proposed resolution
In normal cases we will want both cancellation and invalidation, so we will focus on that for now. The default cancellation UI will do both.
In case we want to invalidate a single declaration without affecting anything else, already an admin can edit a declaration and change the state to invalid, adding evidence. This isn't a streamlined / friendly UI, but also it's an unusual edge case.
Remaining tasks
We still need to write tests that cover #3560177: Safer admin overrides, which wasn't possible until we got this one right.
User interface changes
API changes
Data model changes
Issue fork gift_aid-3572561
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #2
jonathanshawI agree, let's create seperate routes here, it's cleaner and more maintainable. I'd be tempted to call it an "invalidation" as invalid/cancelled is the distinguishing terms used in the guidance 3.8
Comment #3
jonathanshawComment #4
jonathanshawI think it's simpler (and fine for 99% of use cases) to limit the focus to a single declaration.
Comment #5
adamps commentedComment #6
adamps commentedComment #8
adamps commentedSorry wrong issue.
Comment #9
adamps commentedFor staff
Potentially we don't need a new route. It's just a matter of editing a declaration and changing the validity. The only catch is that the validity field will be locked. We could have a checkbox for invalidate that maps to a change in validity inside the forms save.
There is a similar case if staff manually record a confirmation (e.g. sent by Royal Mail). They would need to alter confirmation_date, but that's locked. And for good reason, as the don't want to allow removing the confirmation_date from a claimed declaration.
Another solution then is to make the locking more subtle: some field transitions are OK, some are blocked. We could compare with original in the verify function.
For self
I guess we need to duplicate
gift_aid_user.context.cancelincluding everything that follows on, and everything that links to it. Should we also have a link to invalidate on the Gift Aid tab, if there is a suitable declaration? It would be a bit hard to explain/describe it. But if we don't, then only way to access it is via the email link. Perhaps the donor distrusts the legitimacy of the email and prefers to navigate to the site themself (sometimes I might do that).Do you mean we don't need a route parameter that takes a declaration id? What about the other 1% of the time😃? I guess we invalidate them all??
I realise that I was just a moment ago in favour of the above, but now I look at the duplication and extra complexity I am having doubts. We could just put a simple choice on the cancel page, as described in #3570296: Improve self-cancellation UI. Most of the time there wouldn't even be a choice as only one option would be available.
Comment #10
adamps commentedActually we can keep #3570296: Improve self-cancellation UI separate, we don't have to do it now.
OK we can do it like this:
DECLARATION_INVALIDATED. That's rather similar to DECLARATION_INVALID, so we could instead say denied, rejected, or refuted. ("Invalidated" potentially doesn't carry the specific meaning relating a negative of a confirmation??).CancellationForm, remove the "Retroactive" checkbox. Put an explanation that this form is only for forward cancellation, and please edit the declaration itself for invalidation.staticfromRecordBase::getCriticalFields()(we don't use it statically anyway). ChangeDeclaration::getCriticalFields()so that it adds 'confirmation_date' only ifisConfirmationSent(), and adds 'validity' only ifisClaimable()is TRUE.DonorCancelFormto remove$startDate. EditgetDescription()to use the text from the final 2 bullets in the previous comment. InsubmitForm(), if$this->retroactive</ code> is TRUE, then instead of creating a cancellation, we find all declarations with <code>canRetroactivelyCancel()and set them to invalidated.Comment #11
jonathanshawA. I agree that setting DECLARATION_INVALIDATED works. But I'm also not sure we need to do that.
B. Retroactive cancellation is a thing we need to support anyway, for reasons given in #3557360: Support retraction. And from a data model perspective that seems a superset of invalidation, so adding DECLARATION_INVALIDATED seems to add complexity to the data model.
C. If staff receive an email or phone call within the 30 days after confirmation is sent, saying no don't claim gift aid, they need to record that. They will naturally turn to the normal cancellation UI to do that. The effect of this cancellation should be to cancel retrospectively any declaration that requires confirmation and has had the confirmation sent within 30 days, and prospectively any other older declaration.
D. C seems to me like an excellent argument in favor of DECLARATION_INVALIDATED. Having this new status allows us to invalidate some declarations while simultaneously registering a prospective cancellation of others.
E. If a donor denies a gift aim confirmation, it's likely that they also wish to cancel previous ongoing declarations and will be unhappy if those go on although the guidelines do not explicitly forbid that. So the pattern of C holds for responses to confirmation emails too.
Therefore I think the solution is this:
- reporting a cancellation (staff/self/from-confirmation-email) is always something that happens at the donor level, not the declaration level
- always create a cancellation entity, it's a sensible and consistent audit trail
- whenever a cancellation is recorded, by any means, update any declarations whose status is invalid without confirmation and for which a confirmation was sent within 30 days, and change declaration status to invalidated.
- use the ordinary cancellation form for ordinary donors coming from a confirmation email to deny confirmation
- when ordinary donors cancel or invalidate, the start date is forced to now, they don't get allow retroactive anyway, that's a staff thing.
Comment #12
adamps commentedIn A&B you seem to be against DECLARATION_INVALIDATED and in C-E in favour, so I'm a bit confused. I absolutely agree that we wouldn't use DECLARATION_INVALIDATED for retroactive (backdated) cancellation - perhaps that's what you are saying too?
I would like to create clarity in our discussion in terms of data model vs user interface.
Good idea. I agree with your suggestion that at a UI level in the main-line cancellation should be done via the donor.
However we don't necessarily need to eliminate more subtle choices for the admin. If they wished to invalidate one declaration specifically but not any others, then they could potentially still edit that declaration. We wouldn't have to write any extra code to allow it - rather we'd have to write extra code if we wanted to block it.
This matches what I was suggesting, good.
Absolutely I agree, donors don't get to choose a date. This is already true, and I have no wish to alter it. Even if we do #3570296: Improve self-cancellation UI they still don't get to choose a date.
These 2 seem to imply that at a data-model level, we would disallow invalidation without cancellation or vice versa. It potentially creates a limitation built into the design that would be difficult to reverse.
What if a donor calls and says:
I feel that
Another point is that currently the design supports flexible contexts (which you have been very enthusiastic about😃). If a cancellation is discovered to be on the wrong donor, it can be moved and everything automatically recalculated in
calculateCancellation()- the calculation takes no function arguments and is not time-based so can be rerun with identical results at any time. Likewise, donors can potentially be merged or separated.The changes here would lose the possibility to recalculate. We wouldn't know whether or not to invalidate, as wouldn't know what the declaration state was at the time of cancellation. Cancellation is global to the Donor, but Invalidation is specific to certain declarations, normally precisely one. So there is a good reason for storing them in the corresponding location.
So in summary of a long comment, I propose to keep the data model as I described it, however I am supportive of your suggestions to simplify the UI. If you are happy with that, the main remaining item is to clarify what each role (Admin/Staff/User) will see on the cancel form:
DonorCancelForm::getDescription()Comment #13
adamps commentedOK I have had some more ideas. I believe I see a way to use a cancellation entity to include the case of invalidation while avoiding the problems I've mentioned above.
In the normal simple case for invalidation, there will be precisely one recent declaration with a recent a confirmation (both within 30 days). The guidelines state that cancellation should be be "as if never made". We can handle this in
Declaration::calculateCancellation(). We add a new test for$cancellation->getReceivedDate()is between 0 and 30 days after$this->getConfirmationDate()andvalidity == DECLARATION_VALID_IF_CONFIRMED, and if so set$result = $this->getStartDate()(and we might as wellbreakfrom the loop). NB we don't change validity - it remains atDECLARATION_VALID_IF_CONFIRMED. In this way, I believe we keep the flexible contexts, and the ability to recalculate later and get the identical result. We are safe no matter what order records are entered and for example if recovering a backup on a new server, at the end we can just callcalculateCancellation()on everything.I believe this code will satisfy your requirement C. B is easy - we just make a cancellation with a start date in the past. E will work the way you are asking too.
However, as I pointed out in my previous comment, I'm not really convinced that the world is always going to be as simple as suggested by E.
This reinforces your comment, that some of the cases here are hard. I'm not proposing to solve them all right now, but I would like to have at least an idea that our design can support a solution later.
1) We could have a flag on the confirmation entity for "proactive only" (and perhaps another option "retroactive only").
2+3) We can set the validity to invalid or invalidated on specific declarations. Or if you prefer, have cancellations with a reference to a specific declaration (but that seems harder to me).
4) Perhaps we shouldn't send out the default confirmation mail in these cases, but instead leave the staff to resolve the process manually?
So overall I believe this design works and it matches your preferred outcomes.
Comment #14
adamps commentedComment #16
adamps commentedI raised #3573666: Flexibile cancellation and #3573667: Staff cannot record a confirmation for things that came up in the comments.
Ready for review please.
Comment #17
jonathanshawMy last comment collided with yours, I was going to say:
I find that a compelling objection to what I was suggesting.
Nice. As discussed elsewhere, in general I feel more confident about dynamic, live approaches like this rather than rigid saved data that is more complex to unpick in edge cases, so that attracts me about what you're saying. And good spot about flexible contexts too.
This basic mechanism of "record a cancellation, apply it retroactively to anything subject to that and prospectively to other things" seems good to me.
Yes. I feel we're probing edge cases that the HMRC guidelines don't consider.
I think the overriding principle in these edge cases should be that what happens matches what we tell the donor should happen. In particular we need to be very careful about what we promise the donor about retrospective cancellation and never underdeliver on that; if in doubt, underpromise and overdeliver.
I think currently we violate this with DonorCancelForm::getDescription(), where we promise total retroactive cancellation if there's one single declaration that can be retroactively cancelled?
I think we can handle this here by not being specific about whether there's retroactive cancellation when it's not obvious there is. We could say that this is the case if there's more than one declaration, but actually even that is not straightforward. More old declarations could be input later, or moved from another context. So maybe we just don't mention retroactivity on the self-cancel form?
It does seem possible in principle to precalculate the impacts of a cancellation as a range of dates that will be affected (for date based declarations). I think we could regard that as our "our design can in principle support this" solution, but definitely ignore it for now. It's hard to communicate well, beyond the 99% use case, and has risky gotchas.
I'm finding this stuff rather hard to think about!
Comment #18
jonathanshawSo my one review point is this:
we seem to have lost the ability for an admin to retroactively cancel gift aid for a time period per #3557360: Support retraction?
Comment #19
adamps commentedI feel that #3573666: Flexibile cancellation is a good place to continue discussion of your concerns from #17. Yes you are right about
DonorCancelForm::getDescription()and this is similar to point 4) of the above issue. I pasted your paragraphs from here onto that issue. Also there is #3570296: Improve self-cancellation UI which could be a good place for the donor UI side of things.I believe it's fine. In our new terminology, retraction as per #3557360: Support retraction is not an invalidation, but a backdated cancellation. This feature is not required by HMRC only by human kindness. If the admin clicks the new override button, then they can enter a start date in the past. They could also enter an end date.
OK to commit here, and continue on the other issues?
Comment #20
jonathanshawFine to commit here. I was just puzzled by why this MR was removing the 'retro' element from DonorCancelForm. Probably it will all make more sense to me when I test it manually.
Comment #21
adamps commentedThe retro element was something I put in speculatively in #3560177: Safer admin overrides then we agreed it was wrong and we would remove it here.
Comment #23
adamps commented