# Pricing Engine Foundation

## Scope

This foundation resolves customer selling prices for outbound SMS segments. It does not contain provider cost, margin, plans, billing, payments, invoices, a Pricing UI, wallet debits, or message-acceptance enforcement. SMS behavior remains unchanged until a later Commercial Enforcement milestone.

All prices are positive integer TZS minor units. No float or third-party money library is used. A quote multiplies the resolved per-segment unit price by authoritative `total_parts` using checked integer arithmetic.

## Destination model

Pricing uses the canonical digits-only E.164 value returned by `SmsAddress::internationalRecipient(...)->value`. Stored prefixes contain 1–15 digits without `+`. The engine does not infer country, operator, mobile network, portability, or provider data.

Resolution generates at most 15 exact prefix candidates from the full destination down to one digit. At most three indexed queries are attempted in precedence order. Each uses `IN` equality candidates and returns no more than 15 effective matches; PHP selects the longest match without SQL sorting. The engine never scans all rates with `recipient LIKE CONCAT(prefix, '%')`.

## Rate-card model and precedence

`price_books` has one book per scope/currency, enforced by `scope_key + currency_code`. The three scopes share the same schema:

1. application (`application:{id}`);
2. tenant (`tenant:{id}`);
3. platform (`platform`).

Within the first scope containing a match, the longest matching prefix wins. Ownership fields and foreign keys prevent an application book from crossing its tenant. Books are active or inactive; inactive books are ignored for new and historical resolution.

`price_book_rates` are immutable effective versions. `effective_from` is inclusive, `effective_until` is exclusive, and null means open-ended. Adjacent windows are valid. Adding a version locks its price-book row and rejects any overlapping active interval for the same book and prefix. The exact book/prefix/from unique constraint provides an additional database guard. This lock serializes concurrent conflicting schedules without locking ordinary quote reads.

An already-effective rate cannot be edited, deactivated, or deleted. A new price is a new version. Only future scheduled rates may be deactivated. Historical quotes are immutable DTO values and are not re-resolved or persisted to messages in this milestone.

## Services and operations

- `PricingResolver` performs one to three bounded indexed resolution queries and returns an immutable unit-price result.
- `PricingQuoteService` validates the E.164 recipient and parts, checks multiplication overflow, and returns an immutable quote snapshot.
- `PricingAdministrationService` creates scoped books, schedules rates, and deactivates books or future rates transactionally with safe audit events.
- `PricingReadService` provides bounded book and effective-window reads.

Artisan commands are the only administration interface: `gateway:pricing:create-book`, `add-rate`, `deactivate-book`, `deactivate-rate`, `list`, and read-only `quote`. They accept public references and never expose internal database IDs. There are no pricing web or public API routes.

## Future commercial transaction seam

A later milestone may execute the following inside one outer transaction: normalize recipient; calculate `total_parts`; resolve an immutable quote; lock wallet; check balance and Usage & Limits; persist message and SMS row; persist usage; persist a wallet debit referencing the quote; persist accepted event and outbox; commit. Pricing resolution is read-only and opens no transaction, so it is compatible with that future outer transaction. This sequence is not active today.

## Deployment and rollback

1. Stop the SMPP runtime.
2. Back up the database.
3. Deploy code and run pricing migrations.
4. Configure books and rates explicitly; migrations create no prices and no zero-price fallback.
5. Verify representative destinations with the quote command.
6. Clear/rebuild application caches.
7. Restart the runtime and verify SMS submissions remain unchanged.

Rollback has no live-charging dependency. Preserve historical price rows in the database backup before rolling back schema. Stop pricing writers, roll back the migration, then redeploy the prior code. No wallet or message records require reversal because pricing is not connected to them.
