Skip to content

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

MethodPathPurpose
PUT/v1/entitlements/syncMaterialize billing-service entitlement snapshots for an account.
GET/v1/entitlements/by-account/{accountId}List entitlements by account and optional organizationId.
POST/v1/usage/checkCheck a requested amount without mutating counters.
POST/v1/usage/consumeIdempotently 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/resetReset matching counters to zero.
PUT/v1/limit-overridesUpsert account or plan limit overrides.
POST/v1/events/billing-entitlements-updatedConsume billing.entitlements.updated.v1 from billing-service.

Legacy-Derived Resource Keys

The foundation mirrors the legacy AZ Credit quota keys:

  • credits_monthly
  • credits_hourly
  • credits_weekly
  • tokens_monthly
  • questions
  • storage_bytes
  • ocr_pages_monthly
  • batch_import_pages
  • concurrent_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 caseResource keysNative API shape
AI question generation and question solutioncredits_monthly, credits_weekly, credits_hourly, tokens_monthlyPOST /v1/usage/check before provider work; future debit orchestration stays outside usage-service.
OCR/import workocr_pages_monthly, batch_import_pagesPOST /v1/usage/check or POST /v1/usage/consume before import/OCR jobs run.
Storage and file/course assetsstorage_bytesPOST /v1/usage/check with the catalog window supplied when enforcing plan storage caps.
Question bank sizequestionsPOST /v1/usage/check against the lifetime authored-question count.
Course/import concurrencyconcurrent_jobsPOST /v1/usage/check with the catalog concurrency window supplied by the caller.
Admin exceptionsany quota resourcePUT /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:

WindowPeriod startPeriod end
HOURTop of the UTC hour.Start plus one hour.
DAY00:00:00Z for the UTC date.Start plus one UTC day.
WEEKMonday 00:00:00Z for the UTC week containing the request time.Start plus seven UTC days.
MONTHFirst day of the UTC month at 00:00:00Z.First day of the next UTC month at 00:00:00Z.
LIFETIME1970-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=1

Optional 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

Go-platform documentation is generated from repository Markdown.