# Self-Service Now mobile API v1

HTTPS JSON endpoints for the native mobile applications. They never expose
database credentials, cost prices, internal payment secrets, or let a mobile
client connect directly to MySQL. The iOS client displays these responses with
native SwiftUI; these endpoints are not web pages and are not used in a WebView.

## Nearby stores

`GET /kiosk_dev/mobile_api/v1/stores/nearby.php`

Required: `latitude`, `longitude`. Optional: `radius_km` (default 25, maximum
100) and `limit` (default 20, maximum 50).

Active stores with coordinates are returned by distance. A store can be shown
for discovery while `ordering.available` is false. A client must never allow
checkout unless that value is true.

## Selected-store menu

`GET /kiosk_dev/mobile_api/v1/stores/menu.php?store_id=2`

The response contains the explicitly selected store, mobile-visible categories
and items, and a normalized modifier graph. Items reference root
`modifier_group_ids`; options can reference conditional `child_group_ids`.

Money is represented as integer minor units. For SGD, `690` means SGD 6.90.
Media URLs are HTTPS. Native apps do not need browser CORS headers.

## Authoritative order quote

`POST /kiosk_dev/mobile_api/v1/orders/quote.php`

Requires `Content-Type: application/json`. The body contains `store_id`, a
`fulfillment` object and one to thirty item lines. Each line supplies only a
menu item ID, quantity, selected modifier option IDs and an optional note.

The server reloads the active store, hours, closure, fulfilment mode, menu
prices, modifier relationships and tax rates. Client-submitted prices and
totals are never accepted. The response uses integer minor units and includes
a SHA-256 fingerprint of the normalized, server-priced order.

Example request:

```json
{
  "store_id": 2,
  "fulfillment": {"mode": "takeaway"},
  "items": [
    {
      "menu_item_id": 66,
      "quantity": 1,
      "modifier_option_ids": [1],
      "note": ""
    }
  ]
}
```

## Native HitPay checkout

`POST /kiosk_dev/mobile_api/v1/payments/hitpay.php`

Uses the same order body plus a new UUID v4 `client_request_id`, a supported
`payment_method` (`card`, `paynow`, or `apple_pay`) and an optional `customer`
object. The server prices the order again, snapshots it, then creates a HitPay
request using the protected server credential. Repeating an identical UUID is
idempotent; reusing it for different content returns a conflict.

`GET /kiosk_dev/mobile_api/v1/payments/status.php?token={public UUID}` returns
only safe status fields. The app polls this after returning from HitPay; an
order becomes paid only after a valid, environment-specific HitPay webhook is
verified and the server finalizer succeeds.

The protected `mobile.enabled` setting blocks creation of new checkouts but
does not block authentication of webhooks for payments already in progress.
Apple Pay remains unavailable in sandbox and must only be exposed after its
production merchant capability is verified.

## App installation and push notification registration

`POST /kiosk_dev/mobile_api/v1/devices/register.php`

The native app creates a random installation UUID and secret, stores both in
the iOS Keychain, and registers them here. A valid APNs token can be included
after notification permission is granted. Subsequent payment requests send
the same identity in the `X-SSN-Installation-ID` and
`X-SSN-Installation-Secret` headers, allowing every paid order to be associated
with the correct guest installation. A future authenticated member session can
link the same installation to `mobile_installations.loyalty_member_id`; no
phone number is used as an authentication identity.

Apply `mobile_api/migrations/20260724_mobile_push_notifications.sql` before
deploying a notification-enabled app build. Kitchen transitions insert
idempotent `preparing` and `ready` jobs into
`mobile_notification_outbox` in the same transaction as the status update.

APNs credentials stay outside the web root in
`/etc/self-service-now/apns.php`; the private `.p8` key path must also remain
under `/etc/self-service-now/`. The CLI-only dispatcher is:

```sh
php mobile_push_worker.php 50
```

For prompt delivery, use the supplied
`deployment/self-service-now-mobile-push.service`, which runs
`mobile_push_worker.php --watch 50` and checks for new jobs every two seconds.
The worker claims jobs safely, retries temporary APNs failures, invalidates
rejected device tokens, and never logs device tokens or provider credentials.

## Mobile member authentication

Apply `mobile_api/migrations/20260725_mobile_member_auth.sql` after the push
notification migration. Migration filenames are permanent, date-prefixed
history; they are not renamed each day.

Every member endpoint requires the authenticated installation headers. Profile
and logout additionally require the opaque member token. The iOS client sends
it in both `Authorization: Bearer {token}` and `X-SSN-Member-Session: {token}`.
The dedicated header keeps session restoration working on Apache/PHP
deployments that do not expose `Authorization`; both forms accept only the
same 43-character opaque token.

- `POST /kiosk_dev/mobile_api/v1/members/request_otp.php`
  accepts `{"phone":"81234567"}` and returns an opaque challenge ID.
- `POST /kiosk_dev/mobile_api/v1/members/verify_otp.php`
  accepts the challenge ID and six-digit code. Existing members receive a
  session. A verified new phone receives `registration_required: true`.
- `POST /kiosk_dev/mobile_api/v1/members/complete_registration.php`
  creates a new member only from an unconsumed, verified challenge and only
  when terms acceptance and a first name are supplied.
- `GET /kiosk_dev/mobile_api/v1/members/me.php`
  returns the member profile, tier, points balance, and twenty recent ledger
  entries.
- `POST /kiosk_dev/mobile_api/v1/members/logout.php`
  revokes the current session and unlinks the installation.

OTP challenges and session tokens are stored only as hashes. Responses are
non-cacheable. Phone, installation, and IP rate limits are enforced server
side. A new OTP supersedes the previous pending code; five failed attempts
lock a challenge. Sessions use rolling expiry and never claim historical guest
orders.

SMS provider credentials and both HMAC secrets must stay outside the web root
in `/etc/self-service-now/member_auth.php`. The provider endpoint must use
HTTPS and accept form-encoded POST requests. Never commit that file or log OTP
codes, bearer tokens, full phone numbers, or provider responses.
Start from the credential-free
`deployment/member_auth.php.example`, adapt only the protected server copy to
the provider's field names, validate it with `php -l`, and enable it only after
an SMS delivery test succeeds. The configured `success_prefix` must match the
provider's accepted-message response; HTTP success alone is not treated as SMS
acceptance.

The CLI-only schema check is:

```sh
php mobile_api/v1/tools/member_auth_schema_check.php
```

## Optional member email verification

Apply `mobile_api/migrations/20260727_mobile_member_email.sql` after member
authentication. A member account still uses verified phone OTP as its primary
sign-in identity. Email is optional and never blocks account creation.

- `POST /kiosk_dev/mobile_api/v1/members/update_profile.php` updates the
  authenticated member's name, optional email, and separate marketing-email
  preference. Changing or removing an email clears its prior verification.
- `POST /kiosk_dev/mobile_api/v1/members/request_email_verification.php`
  sends a six-digit code to the current unverified email.
- `POST /kiosk_dev/mobile_api/v1/members/verify_email.php` verifies the code
  and marks the current email as verified.

Email codes expire after ten minutes, lock after five incorrect attempts, and
are stored only as HMAC hashes. Resend, member, address, installation, and IP
limits are enforced. A new request supersedes older pending codes. Changing
the member email also supersedes pending codes. Unverified emails are not
passed into payment/receipt identity snapshots.

DigitalOcean stores its HMAC keys, relay URL, and relay token outside the web
root in `/etc/self-service-now/member_email.php`. Start from
`deployment/member_email.php.example`. AwardSpace hosts the credential-free
relay template from `deployment/awardspace/member-email-relay.php`; its real
configuration and matching bearer token must stay outside AwardSpace's public
directory. The existing receipt relay remains unchanged.

The CLI-only schema check is:

```sh
php mobile_api/v1/tools/member_email_schema_check.php
```

## Read-only rewards catalogue (Milestone 4D.1)

`GET /kiosk_dev/mobile_api/v1/members/rewards.php` requires the registered
installation headers and an authenticated member session. It returns the
active loyalty programme, member points/tier snapshot, and catalogue rewards
with store assignments and one of these eligibility states:

- `available`
- `insufficient_points`
- `tier_locked`
- `upcoming`

This endpoint is intentionally read-only. It does not claim rewards, reserve
or deduct points, issue coupons, alter checkout totals, or write to the
loyalty ledger. The CLI-only deployment check is:

```sh
php mobile_api/v1/tools/loyalty_rewards_schema_check.php
```

## Future-order member attribution

Apply `mobile_api/migrations/20260726_mobile_member_order_attribution.sql`
after the member-authentication migration. Existing payment requests and
kitchen orders remain unattributed; there is deliberately no historical
backfill.

Payment creation accepts the member session as optional authentication:

- no member header creates a guest payment;
- a valid member header snapshots the server-resolved member ID and contact
  details on the payment request;
- an invalid, expired, malformed, or conflicting supplied member token rejects
  the checkout instead of silently treating it as a guest order.

The client never submits or chooses a member ID. When HitPay confirms payment,
the shared finalizer copies the snapshotted member ID to `kitchen_orders` and
the existing member snapshot fields on `counter_bills`. Signing out later does
not change prior attribution. This milestone does not award points; earning
and database-enforced ledger idempotency belong to Milestone 4C.

The CLI-only schema check is:

```sh
php mobile_api/v1/tools/member_order_attribution_schema_check.php
```

## Server-only pricing smoke test

From the `kiosk_dev` directory on the server, run:

```sh
php mobile_api/v1/tools/quote_smoke_test.php
```

The script is CLI-only and deliberately bypasses the availability switch so
store 2 pricing and modifier validation can be tested before opening ordering.
It never creates a payment or order and cannot be opened over HTTP.
