Skip to main content

Hardware Smoke Runbook (Connected-Device Execution Plan)

Owner: Android/Terminal QA · Created: 2026-06-21 · Status: active execution

This is the single comprehensive plan for proving PeakPOS on real hardware. It consolidates:

  • the live device bench (three Android devices connected via ADB on 2026-06-21),
  • the peripheral capability matrix derived from DevicePlatformFactory and the per-platform overrides in apps/android/app/src/main/java/com/myriad/pinpointpos/device/,
  • every Current Changes Tracker item marked "needs testing" (Monday board 18417721170: Status = Needs manual test or Manual Test = Required / Blocked),
  • the open hardware-relevant GitHub issues.

It supersedes the "no device connected" status recorded in checklists/android-pos.md (2026-06-07 ADB inventory). For exhaustive per-surface negative-case matrices (mTLS denial, idempotency boundaries, scope checks) this runbook links to the existing checklists rather than duplicating them.

Record per-case outcomes in the Execution log (section 9); the case entries below describe what to prove, not a checkbox state. Monday items are referenced as M:<itemId>https://myriad-peak.monday.com/boards/18417721170/pulses/<itemId>.


1. Device bench inventory (verified 2026-06-21)

#DeviceOSSerial (transport)Auto-detected platformCurrent buildNotes
D1LANDI C20ProAndroid 13 (SDK 33)2589CCM07231 wireless (t4; dup t5)LandiDevicePlatform (OmniDriver com.sdksuite.omnidriver)0.0.19 / vc 2750 (release)Device-admin → adb uninstall fails; no .test pkg; VP3350 card reader; customer-display flicker is a known vendor com.ms.msdisplay crash
D2Nexgo N92Android 13 (SDK 33)N92 USB (t9; wireless dup t10)NexgoDevicePlatform (Build.MANUFACTURER == "Nexgo")0.0.0-dev / vc 1 (DEBUG).test pkg present; Nexgo-specific App Check path; no cash-drawer capability exposed
D3Samsung Galaxy Tab A9+ (SM-X230)Android 16 (SDK 36)R5GL32DXJ2L USB (t6)GenericDevicePlatform (fallback)0.0.0-dev / vc 1 (DEBUG).test pkg present; no built-in peripherals — external BT/USB only; best big-screen customer-display surface

1.1 Capability matrix (from source)

Source of truth: DevicePlatform overrides per platform class.

A capability is only usable in-app when the platform's has<X>() returns true — MainActivity gates each peripheral on it. The table reflects in-app behavior on current main, not just whether a <X>() object can be constructed.

CapabilityD1 C20Pro (LANDI)D2 N92 (Nexgo)D3 Tab A9+ (Generic)
Card reader (card-present)yes — LandiCardReader (VP3350)yes — NexgoCardReaderno — external only
Receipt printeryes — Landi 58 mm built-inyes — Nexgo thermalno — BT/USB external
Label printerexternal Zebra (BT)built-in text-onlyNexgoLabelPrinter strips ZPL (NexgoZplUtils); barcodes are not scannable → use external Zebrano — BT external
Cash draweryes — LandiCashDrawer (wired via #1770)no — platform exposes no cashDrawer()no — platform exposes no cashDrawer()
Barcode scannerno (in-app)hasBarcodeScanner() is false, so the wedge is never subscribedyes — NexgoBarcodeScannerno (in-app) — Generic exposes no scanner
Beeper / LEDyesnono

In-app capability gates (current main): barcode scanning works in-app only on D2LandiDevicePlatform.hasBarcodeScanner() is hardcoded false and GenericDevicePlatform exposes no scanner, so D1/D3 never subscribe the keyboard wedge (the same gate pattern that #1770 fixed for the LANDI cash drawer; the scanner gate is still open). Cash drawer works in-app only on D1 (hasCashDrawer() true since #1770); D2/D3 expose no cashDrawer(). HW-20 / HW-40 device scope follows these gates.

1.2 ADB targeting cheat sheet

Transport IDs drift across reconnects — always pin by serial. Duplicate connections (D2 USB+wireless, D1 two TLS) make bare adb install ambiguous.

D1=2589CCM07231            # LANDI C20Pro  (wireless; use the primary TLS entry)
D2=N92 # Nexgo N92 (USB preferred)
D3=R5GL32DXJ2L # Samsung Tab A9+ (USB)

adb devices -l # re-derive transports each session
adb disconnect adb-0123456789ABCDEF-IEKY4S._adb-tls-connect._tcp # drop redundant N92 wireless link
adb -s "$D1" shell ... # target a device explicitly

2. Pre-flight (once per session, before any test case)

Two distinct artifacts — do not conflate them:

  • Release sign-off APK = the explicitly signed output produced from //apps/android:android_app_prod_unsigned.apk (R8/ProGuard, release resources, prod.keystore). prod.keystore is written from Secret Manager and is CI-only (android.yml); it is absent on this bench, so the release-signed APK cannot be produced locally. The release/ProGuard sign-off is a CI-gated artifact.
  • Local functional build = //apps/android:android_app_local_staging (dev.keystore, debuggable, 0.0.0-dev). This is what installs on the bench for functional cases — it is not the ProGuard/release build, so it does not cover R8-shrink-specific behavior.

Match every claim to the artifact under test.

  • PF-1 Clean ADB. Run adb devices -l; drop the redundant N92 wireless link (cheat sheet above). Confirm exactly one usable target per physical device.
  • PF-2 Build. Local functional (debug) build for the bench: bazel build //apps/android:android_app_local_staging (ANDROID_NDK_HOME/ANDROID_HOME set). The true release APK //apps/android:android_app_prod_unsigned.apk plus explicit production signing is the R8/ProGuard check and is CI/keystore-gated — do not claim ProGuard sign-off from the debug build. Record APK path + SHA + which artifact it is.
  • PF-3 Install. The debug build (dev.keystore) installs in place: adb -s "$D2" install -r -d <apk> · adb -s "$D3" install -r -d <apk>. If you instead deploy the CI release APK (prod.keystore), its signature differs from any installed dev.keystore build, so -r is rejected — adb -s "$Dn" uninstall com.myriad.pinpointpos first (clears app data). D1 is device-admin (uninstall blocked) and runs a release build today: keep D1 on its release APK, or remove device-admin in Settings before replacing it — never adb uninstall D1.
  • PF-4 App Check + staging. The App Check provider depends on the build. The debug build (or enable_debug_app_check=true) uses the debug provider → register each device's App Check debug token. The explicitly production-signed release build sets non-debug App Check: D1/D3 use Play Integrity, D2 uses the Nexgo HMAC path — debug tokens do not apply there. Match the token/provider to the build under test and point the app at staging; a provider mismatch can masquerade as a credential block.
  • PF-5 Automated device smokes (debug build). bazel test //apps/android/app/src/androidTest:android_app_launch_smoke_test and bazel test //apps/android/app/src/androidTest:gateway_sdk_activation_cache_device_smoke_test both depend on //apps/android:android_app_local_staging and install bazel-bin/apps/android/android_app_local_staging.apk — i.e. they exercise the debug build (not the release APK) and will reinstall it (redeploy your release APK afterward if you had one). On a multi-device bench both fail with "multiple devices" unless you set ANDROID_SERIAL=<serial> — run one invocation per D1/D2/D3. D1 also needs the .test APK (same dev.keystore) for the activation-cache smoke. A cache_hit proves encrypted persistence; a live-activation timeout is a device dependency, not a persistence failure.

3. On-bench test catalog (Bucket A — provable on D1/D2/D3 now)

Each case lists priority · target device(s) · Monday/GH refs · steps · expected. Capture per-device pass/fail + evidence in the section 9 execution log.

Session & lifecycle

HW-01 — 30-second background / relogin grace · P0 · D1·D2·D3 · M:12273180051, M:12273262760 · PR #1756

  • Steps: Sign in; background the app < 30 s → foreground; then background ≥ 30 s → foreground.
  • Expected: < 30 s resumes with no prompt; ≥ 30 s shows PIN-only lock overlay; Firebase auth + POS state preserved behind overlay; any operator PIN unlocks.

HW-02 — Launch, login, PIN, shift baseline · P0 · D1·D2·D3

  • Steps: Cold launch → terminal selection → operator login → PIN unlock → start shift.
  • Expected: Reaches sale screen; shift open recorded server-side. See android-pos.md for negative cases.

HW-03 — Catalog refresh without app restart · P1 · D1·D2 · M:12327661562 · PR #1739

  • Steps: Edit a product/price backend-side; return to the POS sale screen.
  • Expected: Change appears without restarting the app.

Payments

HW-10 — Card-present sale · P0 · D1 (LandiCardReader/VP3350)·D2 (NexgoCardReader)

  • Steps: Ring a sale → tender Card. Run tap, chip, swipe. Exercise approve, decline, and cancel mid-read.
  • Expected: Reader prompts; approval prints/records; decline + cancel handled cleanly; transaction lands in backend. (D3 has no built-in reader — card tender only with an external reader.)

HW-11 — Refunds & returns manager approval matrix · P0 · D1 · M:12273284367, M:12273262586

  • Steps: Refund a prior sale via card-present, cash, and split tenders; trigger manager approval.
  • Expected: Each refund path requires approval, updates server state, links to the original transaction. Cross-references terminal refund idempotency in android-pos.md.

HW-12 — EBT acceptance decision · P0 (decision) · D1 (EBT-capable) · GH #1766

  • Steps: Determine whether Gateway handles EBT on this terminal; if enabled, run a balance inquiry + EBT tender. See android-pos.md.
  • Expected: Documented answer (in-scope? how?) before any EBT support claim.

HW-13 — Offline queue + recovery (no double-charge) · P0 · D1·D2·D3

  • Steps: Sale with network off → queue → restore network → confirm replay; kill app during a pending payment and recover.
  • Expected: Exactly-once replay, no duplicate charge; idempotency per android-pos.md.

Cash drawer

HW-20 — Cash drawer kick · P0 · D1 only (in-app) · M:12273262559, M:12273284344 · GH #1760

  • Scope: in-app, only D1 has a cash drawer (LandiCashDrawer, wired via #1770). D2/D3 expose no cashDrawer() — the drawer is not reachable from the app on those platforms, so mark HW-20 N/A for D2/D3 unless a separate low-level ESC/POS path is added.
  • Pre-flight (D1): confirm the APK contains PR #1770 or later; record APK variant, version, SHA, branch/SHA, tester, timestamp, and environment. Confirm the physical drawer is connected to the LANDI-compatible drawer port or printer/drawer harness used by the site.
  • Evidence capture: clear existing logcat with adb -s "$D1" logcat -c, start a pinned LANDI logcat capture before testing, and save the output as landi-cash-drawer-hw20.log. Include LandiCashDrawer, LandiDevicePlatform, OmniDriver, and CashBox lines when triaging failures.
  • Steps (D1):
    • Open Settings, Hardware or connected-device diagnostics, Cash Drawer, then tap the drawer test/open action and observe the physical drawer.
    • Complete a small test sale tendered as Cash and observe the drawer at sale completion.
    • If a no-sale/drawer-open action is available outside Settings, run it once and observe the physical drawer.
  • Expected: drawer kicks open via OmniDriver getCashBox().openBox() on each trigger. The UI surfaces a clear failure if OmniDriver or the drawer path fails. The code gate (#1760) is fixed on main (#1770); this case is the physical confirmation required before closing #1760.
  • Failure triage: confirm the device is a LANDI C20Pro, not a Nexgo, generic tablet, or emulator; confirm the drawer/cable/harness opens with a known-good vendor/device test if available; record whether the app hid the cash drawer control, showed the control but failed, or reported success while the drawer stayed closed. Keep #1760 open until the physical drawer opens on-device or a new code/hardware finding explains the failure.

Printers

HW-30 — Receipt print + reprint · P0 · D1 (Landi 58 mm)·D2 (Nexgo thermal)

  • Steps: Complete a sale → print receipt; reprint from Transaction Detail.
  • Expected: 32-col / 58 mm layout intact, no truncation, barcode prints.

HW-31 — Label printer hardware proof · P2 · external Zebra (BT) for scannable; D2 built-in = text-only · M:12273162897, M:12273180162

  • Scope note: D2's built-in printer goes through NexgoLabelPrinter, which strips ZPL to plain text (NexgoZplUtils) — a ZPL barcode will not print a scannable barcode there. Prove the scannable-barcode path on an external Zebra (BT); use D2's built-in only to confirm the text-only fallback renders product/price/date.
  • Steps: pair/connect printer; print product/shelf labels; scan the printed barcode (external Zebra).
  • Expected: scannable barcode on external Zebra; correct product/price/date formatting; offline errors surfaced; no silent fall-through to the receipt printer. D2 built-in: text-only, not scannable.

Barcode scanner & scan-data

HW-40 — Barcode scan into cart · P0 · D2 only (in-app) · D1/D3 blocked by scanner gate

  • Scope: MainActivity subscribes the scanner only when hasBarcodeScanner() is true — that is only D2 (NexgoBarcodeScanner). D1 (hasBarcodeScanner() hardcoded false) and D3 (Generic, no scanner) never subscribe the wedge, so scans don't reach the cart. Mark D1/D3 blocked on the in-app scanner gate (an open gap analogous to the #1770 cash-drawer fix — flag for a follow-up).
  • Steps (D2): scan UPCs into cart with the built-in scanner.
  • Expected: correct item added; multi-scan and unknown-UPC handled.

HW-41 — PO scan import via AI · P1 · D2 (scanner) + AI · GH #1763 (fixed by #1776)

  • Steps: scan a purchase order; confirm it routes to an AI reorder draft with the right products/quantities.
  • Expected: import succeeds. Code landed on main via #1776; this case is the on-device confirmation.

Customer-facing display

HW-50 — Advertisements idle/between-transaction rotation · P0 · D1·D3 · M:12273284372

  • Steps: Configure the staged ad through the retail checklist, idle the terminal, and complete sales; observe ad rotation + impression capture.
  • Expected: Ads rotate on idle and between transactions; impressions recorded. (D1 flicker = known vendor com.ms.msdisplay issue, not a regression.)

HW-51 — Loyalty entry on display + cross-store balance · P1 · D1 (2-store) · M:12273178045, M:12273288780 · PR #1740/#1721

  • Steps: Customer enters phone/email on the customer-facing display; check loyalty balance reflects cross-store activity across two stores.
  • Expected: Entry captured; per-store + shared balance correct.

HW-52 — Store type + merchant logo on idle · P2 · D1·D3 · M:12273172511, M:12273161853 · PR #1728

  • Steps: Configure store type + merchant logo; observe requested display locations; test unknown/unset store type and missing/corrupt/slow logo.
  • Expected: Correct display; Peak logo fallback on missing/failed image.

HW-53 — Customer review screen · P1 · D1·D3 · M:12273153564 · PR #1687

  • Steps: Complete a sale; confirm the post-sale review prompt renders on the customer-facing display.
  • Expected: Review screen displays and captures input.

HW-54 — Basket + payment-status mirroring / privacy idle · P1 · D1

  • Steps: Build a basket; observe the display mirrors items + running total + payment status; confirm privacy-safe idle screen between customers.
  • Expected: Accurate mirroring; no prior-customer data on idle.

Compliance

HW-60 — Restricted sale + ID scan + manager override + compliance attach · P0 · D2 (scanner) for the ID-scan leg; D1 for display/override · M:12273161616

  • Scanner caveat: the ID-scan leg needs an active barcode scanner, which in-app is D2 only (D1's scanner gate is off — see HW-40). Run the ID-scan portion on D2; the manager-override and customer-display compliance attach can be exercised on D1.
  • Steps: Add an age-restricted item → scan ID (D2) → manager override → confirm compliance record attached.
  • Expected: Allowed/denied flows correct; override audited; compliance evidence stored. See android-pos.md (age verification).

Catalog & products

HW-70 — Product image upload listing on POS · P2 · D1·D2·D3 · M:12327660784 · PRs #1723–#1726

  • Steps: Upload a product image (portal) → confirm it displays on POS catalog/checkout + thumbnail.
  • Expected: Image renders via CDN read path; no WAF block.

HW-71 — Cart/checkout shows all items · P1 · D1·D2 · GH #1764

  • Steps: Build a large cart; verify every line item is visible/scrollable at checkout.
  • Expected: All items shown (tracked bug — confirm fix or capture repro).

Notifications

HW-80 — Owner notification push fan-out + tap routing · P1 · D1·D2·D3 · M:12273262610 · PR #1784

  • Steps: Register device token; trigger an owner-scoped event; receive push; tap to route.
  • Expected: Push delivered to org-owner/store-admin scope; tap opens the correct screen. (Email/SMS channels are modeled-not-sent → ST-05.) See checklists/notifications.md.

Terminal admin

HW-90 — Terminal reset button behavior + PIN · P2 · D1·D2 · M:12273262608 · PR #1727

  • Steps: Invoke the reset button; test wrong PIN then right PIN (or confirm removal if product = delete).
  • Expected: Wrong PIN blocks; right PIN resets; or button removed/hidden per decision.

HW-91 — Terminal store reassignment (support portal) · P2 · portal + D1/D2 · M:12327660782 · PR #1731

  • Steps: Reassign a terminal's store in the support portal; confirm the device reflects the new store and permissions hold.
  • Expected: Reassignment consistent across portal + device.

Provisioning / mTLS

HW-95 — Terminal onboarding + mTLS enrollment + denial matrix · P0 · D1·D2 (needs mTLS proxy/creds)

  • Steps: Onboard a fresh terminal; enroll mTLS cert; confirm terminal-api reachable; run the acceptance + denial matrix.
  • Expected: Valid cert resolves org/store/terminal; each denial stops before mutation. Full matrix in android-pos.md and terminal-mtls-local.md.

3.A Automated harness (bench-pass test infra)

A Bazel-driven harness automates the scriptable legs of HW-20 / HW-30 / HW-40 so a regression is caught off-device and the on-device leg is reduced to a captured logcat assertion + a physical-output human check. The harness only observes/asserts existing behavior — it changes no device capability gate (hasBarcodeScanner, hasCashDrawer, the per-platform DevicePlatform overrides). HW-10 (card-present) and HW-31 (external Zebra label) stay manual — see the table below.

All device-smoke targets are tagged manual + requires-android-device; the on-device runs are external bench gates (they do not run in CI or on a host without a device). Pin the device with ANDROID_SERIAL (see the §1.2 cheat sheet) so the right platform is exercised. Set ANDROID_HOME/ANDROID_SDK_ROOT (and ANDROID_NDK_HOME if needed); if Gateway Maven returns 401/403, run source scripts/gateway-maven-credentials.sh first.

Per-case run commands

# HW-20 cash drawer kick — D1 (LANDI) ONLY (D2/D3 expose no cashDrawer())
ANDROID_SERIAL=2589CCM07231 bazel test --config=android_x86_64 \
//apps/android/app/src/androidTest:hw20_cash_drawer_device_smoke_test --test_output=all

# HW-30 receipt print + reprint — D1 (LANDI 58 mm)
ANDROID_SERIAL=2589CCM07231 bazel test --config=android_x86_64 \
//apps/android/app/src/androidTest:hw30_receipt_device_smoke_test --test_output=all
# HW-30 — D2 (Nexgo thermal); PEAK_POS_HW30_INSTALL=1 installs the local debug build on D2
ANDROID_SERIAL=N92 PEAK_POS_HW30_INSTALL=1 bazel test --config=android_x86_64 \
//apps/android/app/src/androidTest:hw30_receipt_device_smoke_test --test_output=all

# HW-40 barcode scan into cart — D2 (Nexgo) ONLY (D1/D3 are gated off the in-app scanner)
ANDROID_SERIAL=N92 bazel test --config=android_x86_64 \
//apps/android/app/src/androidTest:hw40_scan_device_smoke_test --test_output=all

The HW-30 format/barcode contract is also pinned off-device (no device needed) by a JVM unit target — run it in CI/locally:

bazel test //apps/android/app/src/test:hw30_receipt_format_harness_test

What each target proves vs. what stays human/external

CaseDeviceTargetAutomated assertionStays human / external
HW-20D1 only…/androidTest:hw20_cash_drawer_device_smoke_testlogcat: LandiCashDrawer/OmniDriver/openBox path exercised with no Failed to open cash drawer. (open() logs nothing on success.)The physical drawer opening (vendor signal not in logcat).
HW-30D1 & D2…/androidTest:hw30_receipt_device_smoke_testlogcat: printReceipt calledStarting print...Print completed successfully (i.e. ReceiptPrintService.printReceipt() ran end-to-end on the device printer).Paper legibility + scannable printed barcode.
HW-30none…/test:hw30_receipt_format_harness_test (JVM)32-col / 58 mm layout has no overflow; barcode + format threaded to printDocument(barcode=…); reprint replays identical body+barcode.n/a (off-device contract test; runs in CI).
HW-40D2 only…/androidTest:hw40_scan_device_smoke_test (runs Hw40BarcodeScanDeviceTest)instrumentation: a UPC injected through the production KeyboardWedgeScanner path emits as a barcode (single, multi-scan, unknown-UPC passthrough); best-effort live cart logcat (barcode-length log / ItemAdded).A live catalog scan landing as a priced cart line (needs seeded catalog + signed-in terminal).
HW-10MANUAL (§ HW-10)none — card-present tap/chip/swipe + approve/decline/cancel can't be automated.Entire case is human on a live reader.
HW-31MANUAL (§ HW-31)none — external BT Zebra pairing + scannable-barcode print can't be automated.Entire case is human on an external Zebra.

Honesty notes: the HW-20/HW-30/HW-40 device-smoke targets prove the software invocation path on the device; physical tender, drawer hardware, paper output, and scannable printed barcodes remain human bench gates recorded in §9. The smokes accept PEAK_POS_HW20_LOGCAT / PEAK_POS_HW30_LOGCAT to assert against an already-captured logcat from a live session. No capability gate was changed by this harness — the D1 scanner gate (HW-40) and any future D2/D3 drawer path remain follow-up code items, not harness concerns.


4. Staging / backend-gated tests (Bucket B — not blocked by hardware)

Code is in; the gate is credentials/data, not a device. Record the exact missing env (MANAGEMENT_TOKEN, MERCHANT_TOKEN, ORG_ID, STORE_ID, TERMINAL_ID, terminal mTLS cert/key, receipt recipient) so the next run can go deeper without rediscovery.

CasePriItem(s)What to proveGate
ST-01P0M:12273153384, M:12273284225, M:12273084265 · GH #1758SkanData/mKonnekt export end-to-end: V036, tx-bundler SKAN export, loyaltyId, nested-item totals, paritystaging creds + data
ST-02P1M:12292686128Email and SMS delivery of invoices and receipts; confirm invoice path is code-completeemail/SMS provider + recipient
ST-03P1M:12273162659 · PR #1784, GH #1763Live UPCitemdb lookup (PR uses stubs); confirm-before-save Add Product; Android scan path not yet wiredUPCitemdb access
ST-04P1M:12273161353 · PR #1784Household create/merge/split, loyalty rollups, campaign dedupe in retail + support portalsstaging org data
ST-05P1M:12273262610 · PR #1784Owner-notification email/SMS channel dispatch (push = HW-80)provider send
ST-06P1M:12273084266Account-scoped merchant migration staging/prod GRANTs + downstream smokestaging/prod DDL access

5. External / blocked / unbuilt (Bucket C — not provable on this bench)

Surface these as honest gaps; a green bench run is not full launch coverage.

CasePriItem(s)Why off-bench
EX-01P0M:12273153385iOS POS / iPad Gateway activation — Mac/Xcode + card-present runtime
EX-02P0M:12273284348iOS TestFlight + App Store submission — Apple pipeline. See checklists/ios-pos.md
EX-03P1M:12273161264, M:12273284282Peak Mobile iOS inventory/push/AI (Android side is HW-80/HW-40). See checklists/peak-mobile.md, checklists/mobile-release-evidence.md
EX-04P1M:12273236728Blocked on real Puff n Huff merchant invoices for AI import
EX-05P3M:12273172123, M:12273180185Processor smart routing — not implemented; test after build
EX-06P3M:12273161726Firearms vertical — unbuilt; scope legal/compliance first
EX-07P3M:12273262899Automotive VIN/model vertical — unbuilt; define identity fields first
EX-08P3M:12327633040Intune MDM — scoping only, no customer driver

6. Per-device end-to-end sale smoke (run last)

The integrated proof — run a full real sale on each device:

login → scan items (HW-40) → apply discount → card tender (HW-10) → receipt print (HW-30) → cash tender + drawer kick (HW-20) → customer display correct (HW-50/54) → transaction in backend.

  • D1 C20Pro — full integrated sale.
  • D2 N92 — full integrated sale.
  • D3 Tab A9+ — full sale with external peripherals.

Only this proves the integrated stack; the launch + activation-cache smokes (PF-5) prove startup/persistence only.


7. Coverage gaps & honesty notes

  • Debug vs release: the bench installs the local debug (dev.keystore) build; the explicitly signed release/ProGuard APK is CI/keystore-gated and not buildable here. Functional cases are valid on the debug build, but ProGuard/R8-specific behavior is not signed off until the explicitly production-signed CI release APK is exercised — do not claim release sign-off here.
  • In-app capability gates (not just hardware): barcode scanning is in-app only on D2 (D1 hasBarcodeScanner() is false; D3 Generic exposes none) and the cash drawer is in-app only on D1 (#1770). HW-40/HW-60 ID-scan run on D2; HW-20 runs on D1; the open D1/D3 scanner gate is a follow-up code item, not a tester failure.
  • D2 labels are text-only: NexgoLabelPrinter strips ZPL, so scannable barcodes require an external Zebra (HW-31).
  • D1 cannot run the activation-cache smoke until the .test APK (same dev.keystore) is installed.
  • A launch smoke proves startup, not authenticated workflows, value movement, receipt delivery, peripherals, or staging bindings — keep ST-/EX- items open until their evidence exists.
  • iOS / Mac / Xcode / XCFramework proof is external-only on this host.

8. Traceability matrix (Monday "needs testing" → case)

Monday itemTitle (abbrev)PriCase
12273180051 / 12273262760Android 30s background/relogin graceP0HW-01
12327661562Catalog refresh without restartP1HW-03
12273284367 / 12273262586Refunds & returns manager approvalP0HW-11
12273262559 / 12273284344Cash drawer hardwareP0HW-20
12273162897 / 12273180162Label printer hardwareP2HW-31
12273161616Restricted sale + ID scan + overrideP0HW-60
12273284372Advertisements on customer displayP0HW-50
12273178045 / 12273288780Loyalty profile + customer-display entryP1HW-51
12273172511 / 12273161853Store type + merchant logo idle displayP2HW-52
12273153564Customer review screenP1HW-53
12273262610Owner notification fan-out (push)P1HW-80 / ST-05
12327660784Product image upload listingP2HW-70
12273262608Terminal reset button behaviorP2HW-90
12327660782Terminal store reassignmentP2HW-91
12327662836Retail portal edge cases / shift scheduleP2retail-portal.md
12273153384 / 12273284225 / 12273084265SkanData/mKonnekt exportP0ST-01
12292686128Email + SMS invoices & receiptsP1ST-02
12273162659External SKU/UPC lookupP1ST-03
12273161353Household / customer relationshipP1ST-04
12273084266Account-scoped merchant migrationP1ST-06
12273153385iOS POS Gateway activationP0EX-01
12273284348iOS TestFlight + store submissionP0EX-02
12273161264 / 12273284282Peak Mobile inventory/push/AIP1EX-03
12273236728Puff n Huff invoices (blocked)P1EX-04
12273172123 / 12273180185Processor smart routingP3EX-05
12273161726Firearms verticalP3EX-06
12273262899Automotive VIN verticalP3EX-07
12327633040Intune MDMP3EX-08

Related GitHub issues: #1758 (SkanData), #1760 (cash drawer), #1763 (PO scan), #1764 (cart items), #1766 (EBT).


9. Execution log

Fill one row per case/device as run. Result is one of Pass / Fail / Blocked. Evidence = logcat path, screenshots, backend txn/receipt IDs, APK SHA.

DateCaseDeviceResultTesterEvidence
2026-06-21PF-2/3 build + install (debug)D2, D3Passbench (adb)android_app_local_staging.apk sha 15c07962…; install -r OK
2026-06-21PF-5 launch smokeD2Passbench (adb)launch_smoke_test.sh; boots STAGING, foreground, no FATAL
2026-06-21PF-5 launch smokeD3Passbench (adb)launch_smoke_test.sh; boots STAGING, no FATAL
2026-06-21PF-5 activation-cache smokeD2Passbench (adb)gateway_sdk_activation_cache_device_smoke_testcache_hit
2026-06-21PF-2 R8/ProGuard verify (release)buildPassbenchHistorical standalone R8 check build passed; current gate builds //apps/android:android_app_prod_unsigned.apk and signs it explicitly
2026-06-21PF-4 App Check debug tokenD3Passbench (adb)token 31ebc7e9… registered on peaksuite-staging via App Check API (ADC)
2026-06-21HW-02 login → saleD1Passbench (adb)PIN unlock; live PEAK Beauty terminal; operator sale screen + catalog
2026-06-21HW-71 cart shows itemsD1Passbench (adb)manual item → Cart(1), Subtotal/Total; dual pricing Cash $5.00 / Card $5.20
2026-06-21Checkout / tender flowD1Passbench (adb)tip→customer-display handoff; Gift Card; Key In Card; manager fee-waive
2026-06-21HW-50 / HW-52 customer-display brandD1Passbench (adb)com.ms.msdisplay shows PEAK Beauty / Powered by PEAK POS / Welcome
2026-06-21HW-54 customer-display tip handoffD1Passbench (adb)"Waiting for customer to choose tip on the customer display"
2026-06-21History / transactionsD1Passbench (adb)real sales/refunds/batches + txn detail; 0 FATAL across navigation
2026-06-21Provisioning restore (staging)D2, D3Blockedbench (adb)terminal-api mTLS ConnectionShutdownException; debug build lacks a valid terminal cert

Bench session notes (2026-06-21)

  • D1 (C20Pro, release 0.0.19) is a live, staging-provisioned terminal — the full software-observable POS path passed (login → cart → dual pricing → checkout → tip handoff → tender options; history/transactions; customer-display branding). Physical tender (card read / cash drawer) and completing a real transaction remain human-gated.
  • D2/D3 (local debug builds) pass the launch + activation-cache smokes but cannot restore terminal provisioning against staging: the terminal-api mTLS handshake is dropped (ConnectionShutdownException) even though general internet and DNS for staging-terminal.peakpos.co resolve. Registering the App Check debug token (done for D3) is necessary but not sufficient — the debug build needs re-onboarding to obtain a terminal client cert the staging mTLS LB accepts. Until then the POS screens / Hardware Test page / sale E2E are unreachable on D2/D3.
  • Cash-drawer / printer / scanner Hardware Test page is not in D1's 0.0.19 build (it postdates #1770 + the HardwareTestSection); validating those on-device needs the CI explicitly production-signed release APK deployed to D1.
  • D1's displays are FLAG_SECURE (both the operator screen and the com.ms.msdisplay customer display), so screenshots are blocked — evidence is captured via uiautomator text dumps.