Just an update on this in case anyone else runs into the same problem:
I'm sure purchasing an IAP (from within the Android) app using a promo code DOESN'T trigger "on product owned" (like it does when using a card) but it does trigger "on purchase success". Neither trigger sets "has product" = TRUE though.
Using the promo code in the Google Play Store app (or Apple App Store) works fine.
As a workaround, I've swapped "on product owned" out for "on purchase success" and am now using that to determine whether the user has just bought the IAP or not.
If the user already has the product (i.e. if they've redeemed a promo code in the Store or have previously bought it and have reinstalled) it looks like "has product" = TRUE only if "complete product registration" has successfully completed (Android), and for iOS, "restore product" has also been called. (Not totally related to this issue, but I've used the above info to change my app to automatically restore purchases for both Android and iOS.)
Of course, testing this has been a pain - partly because the app (and each build for Android) needs to have been approved by Apple/Google before IAPs can be tested completely, but mostly because, once a user has the IAP, they can't test purchasing the IAP again. I now have 14 Google accounts!!!