django-afip icon indicating copy to clipboard operation
django-afip copied to clipboard

validate() raises exceptions when it shouldn't

Open juanpsenn opened this issue 3 years ago • 5 comments

validate() is raising an exception even with raise_=False.

In this case (https://github.com/juanpsenn/django-afip/commit/5645421f8ea189780b63b4991c6d38e39664dfc4) I'm testing validating an older receipt (older than the last successfully validated receipt), this should fail but also raises an exception when it shouldn't.

I remember this https://github.com/WhyNotHugo/django-afip/issues/31#issuecomment-601808460 and I think that validate() can easily be transactional. Otherwise, this causes integrity issues in the database if the transaction is not handled outside.

Some questions:

  • Why errors are handled differently?
  • Should validate() be transactional itself?

juanpsenn avatar Mar 14 '22 18:03 juanpsenn

In this case (https://github.com/juanpsenn/django-afip/commit/5645421f8ea189780b63b4991c6d38e39664dfc4) I'm testing validating an older receipt (older than the last successfully validated receipt), this should fail but also raises an exception when it shouldn't.

Thanks for a clear test case. It should not raise an exception in this case; it would make sense to handle this case properly.

An unusual thing about this is that we don't have "one error per receipt" like in other scenarios, just one error for the whole batch.

I remember this https://github.com/WhyNotHugo/django-afip/issues/31#issuecomment-601808460 and I think that validate() can easily be transactional. Otherwise, this causes integrity issues in the database if the transaction is not handled outside.

Yup. I initially though about documenting that a transaction should be started, but handling the transaction inside validate is probably safer.

Why errors are handled differently?

Generally when a receipt is valid, AFIP returns a response with a "success result". When it's somehow invalid, it returns a response with a "Failure result". If you send two receipts, you might get one "success result" and one "failure result".

But for this particular case, it does not return that, but returns an error instead (which is a completely different structure). The error is in a format very similar to the "invalid authentication ticket" kind of error. check_response looks for any error of this style and raises (because if authentication fails we do raise). Failure results are handled inside Receipt._validate.

If you want to use REST as an analogy, think of this as "returning HTTP 200 with a JSON describing a failure" vs returning HTTP 400.

Note that in this case, all receipts are rejected; there's no partial successes.

Should validate() be transactional itself?

Yes. ReceiptQuerySet.validate should run transactional.

WhyNotHugo avatar Mar 14 '22 19:03 WhyNotHugo

Thinking about this further, I think we should drop the raise_ parameter for validate(), and never raise (e.g.: keep the behaviour of raise_=False. Raising in this case is a poor interface.

WhyNotHugo avatar Mar 15 '22 17:03 WhyNotHugo

https://github.com/WhyNotHugo/django-afip/commit/484996d32ab1f11e543abf421326f583ca08111c

WhyNotHugo avatar May 24 '22 10:05 WhyNotHugo

From import this:

Errors should never pass silently. Unless explicitly silenced.

We really should raise an exception for any error. Returning a list of errors is a terrible API. In future Python versions, we can use exception groups.

WhyNotHugo avatar Dec 02 '22 23:12 WhyNotHugo

Obviously this needs documented as a very breaking change in the changelog.

WhyNotHugo avatar Dec 02 '22 23:12 WhyNotHugo