Appearance
Usage Service
usage-service owns feature entitlements, quota counters, limit overrides, and usage check/consume/reset decisions.
Public wallet, subscription, payment hook, AI/import, and storage routes remain legacy-proxied. Native /v1/* routes are for service tests, migration tooling, service-to-service calls, and future gateway adapters only.
Boundary
Owns:
- Materialized entitlement snapshots emitted by
billing-service. - Feature gates for quota resources such as OCR pages, import pages, question counts, storage bytes, token/month caps, and concurrent jobs.
- Usage counters by account, optional organization, resource, window, and period.
- Account-level and plan-level limit overrides.
- Idempotent consume events and deterministic reset behavior.
Does not own:
- Subscription plans, prices, invoices, or billing periods. Those belong to
billing-service. - AZ Credit wallet balances, subscription grants, usage debits, or refunds. Those belong to
wallet-service. - Provider webhooks or payment reconciliation. Those belong to
payment-service. - AI/import/file/question execution. Those services call usage-service before doing work; usage-service does not run their jobs.
Broker Subscriber
When USAGE_EVENT_TRANSPORT=nats or MONETIZATION_EVENT_TRANSPORT=nats is configured, usage-service starts a durable NATS JetStream subscriber for monetization.billing.entitlements.updated.v1. The broker handler reuses the same /v1/events/billing-entitlements-updated usecase and processed-event store as HTTP fallback, so broker redelivery and HTTP replay preserve the same replay/stale-snapshot behavior. Default transport remains HTTP until broker runtime proof and route rollback evidence are complete.
Native Routes
| Method | Path | Purpose |
|---|---|---|
PUT | /v1/entitlements/sync | Materialize billing-service entitlement snapshots for an account. |
GET | /v1/entitlements/by-account/{accountId} | List entitlements by account and optional organizationId. |
POST | /v1/usage/check | Check a requested amount without mutating counters. |
POST | /v1/usage/consume | Idempotently consume usage and append a usage event. |
GET | /v1/usage/counters/by-account/{accountId} | List usage counters by account and optional organizationId. |
POST | /v1/usage/reset | Reset matching counters to zero. |
PUT | /v1/limit-overrides | Upsert account or plan limit overrides. |
POST | /v1/events/billing-entitlements-updated | Consume billing.entitlements.updated.v1 from billing-service. |
Legacy-Derived Resource Keys
The foundation mirrors the legacy AZ Credit quota keys:
credits_monthlycredits_hourlycredits_weeklytokens_monthlyquestionsstorage_bytesocr_pages_monthlybatch_import_pagesconcurrent_jobs
Native check/consume calls default to the legacy wallet helper behavior when window is omitted: credits_hourly uses HOUR, credits_weekly uses WEEK, credits_monthly, tokens_monthly, and ocr_pages_monthly use MONTH, and the other resources default to LIFETIME. Billing entitlement event ingestion preserves the window it receives from the plan catalog; the current default billing seed keeps storage, batch import, and concurrent-job limits on the legacy default LIFETIME window. Real callers should send an explicit window whenever their product workflow needs a narrower bucket.
Real Caller Fixture Coverage
The service tests keep fixture coverage for the current HocTapAZ quota callers:
| Caller case | Resource keys | Native API shape |
|---|---|---|
| AI question generation and question solution | credits_monthly, credits_weekly, credits_hourly, tokens_monthly | POST /v1/usage/check before provider work; future debit orchestration stays outside usage-service. |
| OCR/import work | ocr_pages_monthly, batch_import_pages | POST /v1/usage/check or POST /v1/usage/consume before import/OCR jobs run. |
| Storage and file/course assets | storage_bytes | POST /v1/usage/check with the catalog window supplied when enforcing plan storage caps. |
| Question bank size | questions | POST /v1/usage/check against the lifetime authored-question count. |
| Course/import concurrency | concurrent_jobs | POST /v1/usage/check with the catalog concurrency window supplied by the caller. |
| Admin exceptions | any quota resource | PUT /v1/limit-overrides followed by check/consume; account override wins over plan override and entitlement limit. |
Period Policy
Usage counters are bucketed by UTC calendar boundaries. They are not rolling windows:
| Window | Period start | Period end |
|---|---|---|
HOUR | Top of the UTC hour. | Start plus one hour. |
DAY | 00:00:00Z for the UTC date. | Start plus one UTC day. |
WEEK | Monday 00:00:00Z for the UTC week containing the request time. | Start plus seven UTC days. |
MONTH | First day of the UTC month at 00:00:00Z. | First day of the next UTC month at 00:00:00Z. |
LIFETIME | 1970-01-01T00:00:00Z. | null. |
Route cutover that requires legacy rolling-window parity, such as the old hourly or weekly AI lookback helper, must be handled as a separate migration slice. This service currently locks bucketed UTC periods for deterministic check/consume/reset behavior.
Idempotency
POST /v1/usage/consume requires an idempotencyKey body field or Idempotency-Key/X-Idempotency-Key header. Replays return replay: true, the existing usage event, and do not increment counters a second time.
When a finite entitlement or override applies, consumption rechecks the limit while holding the usage-counter lock. Concurrent unique requests that would collectively exceed a quota result in one USAGE_DENIED response and do not write an over-limit counter or event.
POST /v1/events/billing-entitlements-updated validates source=billing-service and type=billing.entitlements.updated.v1, records processed billing events by source event id, treats duplicate deliveries as replay, and rejects stale older snapshots without replacing current entitlements. The payload must include sourceSubscriptionEventId; usage uses that billing subscription event id as the audit link for both zero-price FREE_TRIAL entitlement snapshots and payment-backed paid-plan snapshots.
The HTTP fallback additionally requires X-Internal-Service: billing-service and X-Internal-Token, matched against USAGE_INTERNAL_SERVICE_TOKEN or INTERNAL_SERVICE_TOKEN. Missing or wrong credentials return 401 before the payload is decoded; an unconfigured receiver returns 503 and does not trust the event.
Verification
bash
GOTOOLCHAIN=go1.25.11 go test ./services/usage-service/... -count=1Optional Postgres repository integration:
bash
USAGE_SERVICE_POSTGRES_TEST_DATABASE_URL='postgres://hoctapaz:hoctapaz@localhost:5433/hoctapaz_platform?sslmode=disable' \
GOTOOLCHAIN=go1.25.11 go test ./services/usage-service/internal/repository \
-run TestPostgresUsageConsumeReplayIntegration -count=1 -v