# Usage and limits foundation

## Accounting decision

Outbound SMS usage is recorded when the message and SMS-specific row are accepted. The usage row, daily/monthly counter updates, accepted event, and transactional outbox event share the message transaction. Units are the segmentation result persisted as `sms_messages.total_parts`; one `outbound_sms` row exists per message. Later outbox, SMPP, retry, receipt, and final-status activity cannot create usage.

## Attribution and uniqueness

`usage_records` is append-only traceability by tenant, application, message, optional authoritative Sender ID, direction, UTC occurrence, UTC date, type, and positive units. `(message_id, usage_type)` is unique. No price, currency, wallet, tax, provider cost, or invoice data is stored.

## Limits and concurrency

Tenant limits aggregate every application; application limits apply only to that application. Daily and monthly limits are independently nullable (`null` is unlimited); zero blocks usage. Both scopes must pass. Periods are half-open UTC calendar days and months.

Configured limit rows are locked in tenant-then-application order inside the acceptance transaction. Current daily/monthly counters are read while those locks are held. Counter rows are inserted idempotently, locked, and incremented in the same transaction. Bulk production and its idempotency completion now share one outer transaction; full-batch prospective units are checked before item creation and any later failure rolls everything back. Database transaction retry handles deadlocks without duplicating message/type usage.

## Read model

`UsageQueryService` returns current UTC day/month totals, remaining units or unlimited state, percent used, the ten leading applications for the current month, and a bounded daily series of at most 90 days. Reads use counters for current periods and indexed usage records for breakdowns.

## Historical backfill

`php artisan gateway:usage:backfill --chunk=500` scans accepted outbound SMS in bounded primary-key chunks. It uses persisted `total_parts` and accepted/created UTC time, inserts idempotently, and increments counters only for newly inserted rows. Missing or non-positive parts are reported by public message reference and skipped; messages are never modified. Run after schema migration and before configuring production limits.

## Public error

Quota rejection returns HTTP 429 `USAGE_LIMIT_EXCEEDED`, message `The outbound SMS usage limit has been exceeded.`, and empty details. It never exposes scope, totals, or limits. `RATE_LIMIT_EXCEEDED` remains request throttling.

## Future integration and operations

Future Wallet and Pricing services attach after prospective-unit calculation and before acceptance, using usage records as immutable quantity evidence. They must not repurpose usage counters as money.

Deployment: stop SMPP runtime; back up the database; deploy; migrate; run the resumable backfill; verify totals; configure limits; rebuild caches; restart runtime; smoke-test single and multipart messages; verify one increment. Rollback code first if enforcement must stop. Schema rollback removes counters, limits, and usage evidence but never messages. Messages accepted while limits were active remain valid; preserve/export usage rows before destructive rollback when commercial reconciliation may need them.
