| Fetch offerings on a fresh install | offerings.current exists and contains the package slots your UI reads. | Catalog > Offerings shows default as current with monthly and annual packages mapped. | Not required for this row. | Mobile |
| Purchase monthly in sandbox | Purchase returns success or refreshed CustomerInfo; hasActiveEntitlement("premium") is true. | Customers or Transactions shows tester ID, appUserId, store transaction ID, product, environment, and active entitlement. | Store notification may arrive later; outbound webhook should receive subscription.purchased or purchase.completed if configured. | Mobile + Backend |
| Force a sandbox renewal | Customer info remains active and the expiration or renewal timestamp advances. | Customers > Timeline or Transactions shows renewal event, product, environment, store transaction ID, and latest event timestamp. | Apple notification or Google RTDN is received and processed; outbound webhook receives subscription.renewed if configured. | QA |
| Trigger grace or billing retry | App shows the intended grace, retry, or locked state from customer info. | Customers > Timeline shows billing retry or grace state for the same tester and product. | Store notification or RTDN is processed; outbound webhook receives subscription.billing_issue when configured. | QA + Backend |
| Cancel or expire sandbox subscription | Customer info refresh eventually removes active access or shows the expected grace state. | Customers > Timeline or Transactions shows cancellation or expiration with the latest event timestamp. | Apple notification or Google RTDN is received and processed for the matching app. | QA |
| Refund or revoke a purchase | Premium access is removed after refresh. | Customers > Timeline or Transactions shows refund, revoke, or voided purchase for the same store transaction ID. | Store notification is processed; outbound webhook receives subscription.refunded, subscription.revoked, or purchase.refunded. | QA + Backend |
| Reinstall and restore | Restore refreshes the correct customer without duplicating grants. | Customers maps the restored transaction to the expected appUserId or anonymous identity. | Not required unless a store notification is emitted. | Mobile |
| Sign in and sync purchases | syncPurchases() refreshes ownership after identity changes without showing a restore prompt. | Customers shows the signed-in appUserId and expected transaction ownership. | Not required unless a store notification is emitted. | Mobile |
| Drain a queued receipt | drainReceiptQueueAndRefreshCustomer() posts retryable receipts and returns refreshed customer state. | Transactions or customer timeline shows the queued receipt resolved instead of duplicated. | Webhook fires only after AppActor processes the receipt; retryable failures stay visible. | Mobile + QA |
| Send Apple test notification | No SDK change required. | Apps > iOS app detail shows Apple webhook state, environment, verified time, test result, and last processed timestamp. | Apple test notification is received and processed for the current URL. | Ops |
| Send Google RTDN test notification | No SDK change required. | Apps > Android app detail shows current Topic ID, package binding, and processed RTDN health. | Google test notification is received and processed for the current Topic ID and package. | Ops |
| Deliver outbound webhook | Backend handler returns 2xx after durable accept. | Webhook delivery detail is delivered, with response code, delivery ID, event ID, and attempt count. | X-AppActor-Signature verifies and duplicate eventId does not duplicate side effects. | Backend |
| Force a webhook retry | Temporarily return 500 or timeout once, then recover to 2xx. | Webhook delivery detail shows failed attempt, retry attempt count, response code or timeout, then delivered status. | Retry uses the same event safely, and idempotency prevents duplicate side effects. | Backend |