Yes, your current design matches the recommended pattern: Redemption Code is a one-way gate for “has this user consumed this shared code?”, while your claim entity is the authoritative state machine for fulfillment, retries, and audit (Redeeming → Redeemed → Granting → Granted / GrantFailed), with Granted as the idempotency guard for duplicate grants. The main thing to watch is call ordering: it’s preferable to write the claim to Redeeming first as an intent record, then call redeemCode(), so any Redeeming with no further progress is detectable and recoverable by your server without depending on the Redemption Code record.
Consolidating the brainCloud-side claim init + redemption into a single CloudCode script is a good fit, provided the script is strictly idempotent and fulfillment stays in your server pipeline. The script should read the claim state, upsert to Redeeming if needed, call redeemCode() (skipping if already Redeemed or beyond), update the claim to Redeemed, and return rich state (claimState, claimId, rewardDataId) so your server can then perform fulfillment and issue a final Granted / GrantFailed update. This gives you a clean two-call pattern from the server to brainCloud on the happy path, while the script’s state checks make retries safe even though CloudCode itself is not transactional.